aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-07-12 17:41:14 +0200
committerFlorian Dold <florian@dold.me>2022-07-12 17:41:14 +0200
commitf11483b511ff1f839b9913c4832eee9109f67aeb (patch)
tree6f4e1c5891a24bbb7500cea3964d3826d2ef87e1 /packages/taler-wallet-core/src/operations
parentb214934b75418d0d01c9556577d9594f1db5a319 (diff)
downloadwallet-core-f11483b511ff1f839b9913c4832eee9109f67aeb.tar.xz
wallet-core: implement accepting p2p push payments
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts52
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/peer-to-peer.ts270
3 files changed, 294 insertions, 30 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 3a9121502..e4eaf8913 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -16,22 +16,46 @@
import {
AmountJson,
- Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus,
- BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms,
- DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp,
- WalletBackupContentV1
+ Amounts,
+ BackupCoinSourceType,
+ BackupDenomSel,
+ BackupProposalStatus,
+ BackupPurchase,
+ BackupRefreshReason,
+ BackupRefundState,
+ codecForContractTerms,
+ DenomKeyType,
+ j2s,
+ Logger,
+ PayCoinSelection,
+ RefreshReason,
+ TalerProtocolTimestamp,
+ WalletBackupContentV1,
} from "@gnu-taler/taler-util";
import {
- AbortStatus, CoinSource,
+ AbortStatus,
+ CoinSource,
CoinSourceType,
- CoinStatus, DenominationVerificationStatus, DenomSelectionState, OperationStatus, ProposalDownload,
- ProposalStatus, RefreshCoinStatus, RefreshSessionRecord, RefundState, ReserveBankInfo,
- ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo
+ CoinStatus,
+ DenominationVerificationStatus,
+ DenomSelectionState,
+ OperationStatus,
+ ProposalDownload,
+ ProposalStatus,
+ RefreshCoinStatus,
+ RefreshSessionRecord,
+ RefundState,
+ ReserveBankInfo,
+ ReserveRecordStatus,
+ WalletContractData,
+ WalletRefundItem,
+ WalletStoresV1,
+ WireInfo,
} from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js";
import {
checkDbInvariant,
- checkLogicInvariant
+ checkLogicInvariant,
} from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
import { RetryInfo } from "../../util/retries.js";
@@ -313,14 +337,12 @@ export async function importBackup(
}
for (const backupDenomination of backupExchangeDetails.denominations) {
- if (
- backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa
- ) {
+ if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher");
}
const denomPubHash =
cryptoComp.rsaDenomPubToHash[
- backupDenomination.denom_pub.rsa_public_key
+ backupDenomination.denom_pub.rsa_public_key
];
checkLogicInvariant(!!denomPubHash);
const existingDenom = await tx.denominations.get([
@@ -535,7 +557,7 @@ export async function importBackup(
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
const contractTermsHash =
cryptoComp.proposalIdToContractTermsHash[
- backupProposal.proposal_id
+ backupProposal.proposal_id
];
let maxWireFee: AmountJson;
if (parsedContractTerms.max_wire_fee) {
@@ -679,7 +701,7 @@ export async function importBackup(
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
const contractTermsHash =
cryptoComp.proposalIdToContractTermsHash[
- backupPurchase.proposal_id
+ backupPurchase.proposal_id
];
let maxWireFee: AmountJson;
if (parsedContractTerms.max_wire_fee) {
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index b6bae7518..55b8f513d 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -35,6 +35,7 @@ import {
ConfirmPayResult,
ConfirmPayResultType,
ContractTerms,
+ ContractTermsUtil,
Duration,
durationMax,
durationMin,
@@ -87,7 +88,6 @@ import {
selectForcedPayCoins,
selectPayCoins,
} from "../util/coinSelection.js";
-import { ContractTermsUtil } from "../util/contractTerms.js";
import {
getHttpResponseErrorDetails,
readSuccessResponseJsonOrErrorCode,
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index e2ae1e66e..658cbe4f7 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -18,25 +18,47 @@
* Imports.
*/
import {
+ AbsoluteTime,
+ AcceptPeerPushPaymentRequest,
AmountJson,
Amounts,
- Logger,
- InitiatePeerPushPaymentResponse,
+ AmountString,
+ buildCodecForObject,
+ CheckPeerPushPaymentRequest,
+ CheckPeerPushPaymentResponse,
+ Codec,
+ codecForAmountString,
+ codecForAny,
+ codecForExchangeGetContractResponse,
+ ContractTermsUtil,
+ decodeCrock,
+ Duration,
+ eddsaGetPublic,
+ encodeCrock,
+ ExchangePurseMergeRequest,
InitiatePeerPushPaymentRequest,
- strcmp,
- CoinPublicKeyString,
+ InitiatePeerPushPaymentResponse,
j2s,
- getRandomBytes,
- Duration,
- durationAdd,
+ Logger,
+ strcmp,
TalerProtocolTimestamp,
- AbsoluteTime,
- encodeCrock,
- AmountString,
UnblindedSignature,
+ WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
-import { CoinStatus } from "../db.js";
+import { url } from "inspector";
+import {
+ CoinStatus,
+ OperationStatus,
+ ReserveRecord,
+ ReserveRecordStatus,
+} from "../db.js";
+import {
+ checkSuccessResponseOrThrow,
+ readSuccessResponseJsonOrThrow,
+ throwUnexpectedRequestError,
+} from "../util/http.js";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { checkDbInvariant } from "../util/invariants.js";
const logger = new Logger("operations/peer-to-peer.ts");
@@ -176,14 +198,22 @@ export async function initiatePeerToPeerPush(
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
- const hContractTerms = encodeCrock(getRandomBytes(64));
- const purseExpiration = AbsoluteTime.toTimestamp(
+
+ const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
Duration.fromSpec({ days: 2 }),
),
);
+ const contractTerms = {
+ ...req.partialContractTerms,
+ purse_expiration: purseExpiration,
+ amount: req.amount,
+ };
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
const purseSigResp = await ws.cryptoApi.signPurseCreation({
hContractTerms,
mergePub: mergePair.pub,
@@ -204,6 +234,13 @@ export async function initiatePeerToPeerPush(
coinSelRes.exchangeBaseUrl,
);
+ const econtractResp = await ws.cryptoApi.encryptContractForMerge({
+ contractTerms,
+ mergePriv: mergePair.priv,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ });
+
const httpResp = await ws.http.postJson(createPurseUrl.href, {
amount: Amounts.stringify(instructedAmount),
merge_pub: mergePair.pub,
@@ -212,11 +249,216 @@ export async function initiatePeerToPeerPush(
purse_expiration: purseExpiration,
deposits: depositSigsResp.deposits,
min_age: 0,
+ econtract: econtractResp.econtract,
});
const resp = await httpResp.json();
logger.info(`resp: ${j2s(resp)}`);
- throw Error("not yet implemented");
+ if (httpResp.status !== 200) {
+ throw Error("got error response from exchange");
+ }
+
+ return {
+ contractPriv: econtractResp.contractPriv,
+ mergePriv: mergePair.priv,
+ pursePub: pursePair.pub,
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ };
+}
+
+interface ExchangePurseStatus {
+ balance: AmountString;
+}
+
+export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
+ buildCodecForObject<ExchangePurseStatus>()
+ .property("balance", codecForAmountString())
+ .build("ExchangePurseStatus");
+
+export async function checkPeerPushPayment(
+ ws: InternalWalletState,
+ req: CheckPeerPushPaymentRequest,
+): Promise<CheckPeerPushPaymentResponse> {
+ const getPurseUrl = new URL(
+ `purses/${req.pursePub}/deposit`,
+ req.exchangeBaseUrl,
+ );
+
+ const contractPub = encodeCrock(
+ eddsaGetPublic(decodeCrock(req.contractPriv)),
+ );
+
+ const purseHttpResp = await ws.http.get(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const getContractUrl = new URL(
+ `contracts/${contractPub}`,
+ req.exchangeBaseUrl,
+ );
+
+ const contractHttpResp = await ws.http.get(getContractUrl.href);
+
+ const contractResp = await readSuccessResponseJsonOrThrow(
+ contractHttpResp,
+ codecForExchangeGetContractResponse(),
+ );
+
+ const dec = await ws.cryptoApi.decryptContractForMerge({
+ ciphertext: contractResp.econtract,
+ contractPriv: req.contractPriv,
+ pursePub: req.pursePub,
+ });
+
+ await ws.db
+ .mktx((x) => ({
+ peerPushPaymentIncoming: x.peerPushPaymentIncoming,
+ }))
+ .runReadWrite(async (tx) => {
+ await tx.peerPushPaymentIncoming.add({
+ contractPriv: req.contractPriv,
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ mergePriv: dec.mergePriv,
+ pursePub: req.pursePub,
+ timestampAccepted: TalerProtocolTimestamp.now(),
+ contractTerms: dec.contractTerms,
+ });
+ });
+
+ return {
+ amount: purseStatus.balance,
+ contractTerms: dec.contractTerms,
+ };
+}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler+http";
+ } else if (url.protocol === "https:") {
+ proto = "taler";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}
+
+export async function acceptPeerPushPayment(
+ ws: InternalWalletState,
+ req: AcceptPeerPushPaymentRequest,
+) {
+ const peerInc = await ws.db
+ .mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
+ .runReadOnly(async (tx) => {
+ return tx.peerPushPaymentIncoming.get([
+ req.exchangeBaseUrl,
+ req.pursePub,
+ ]);
+ });
+
+ if (!peerInc) {
+ throw Error("can't accept unknown incoming p2p push payment");
+ }
+
+ const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
+
+ // We have to create the key pair outside of the transaction,
+ // due to the async crypto API.
+ const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
+
+ const reserve: ReserveRecord | undefined = await ws.db
+ .mktx((x) => ({
+ exchanges: x.exchanges,
+ reserves: x.reserves,
+ }))
+ .runReadWrite(async (tx) => {
+ const ex = await tx.exchanges.get(req.exchangeBaseUrl);
+ checkDbInvariant(!!ex);
+ if (ex.currentMergeReservePub) {
+ return await tx.reserves.get(ex.currentMergeReservePub);
+ }
+ const rec: ReserveRecord = {
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ // FIXME: field will be removed in the future, folded into withdrawal/p2p record.
+ reserveStatus: ReserveRecordStatus.Dormant,
+ timestampCreated: TalerProtocolTimestamp.now(),
+ instructedAmount: Amounts.getZero(amount.currency),
+ currency: amount.currency,
+ reservePub: newReservePair.pub,
+ reservePriv: newReservePair.priv,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ // FIXME!
+ initialDenomSel: undefined as any,
+ // FIXME!
+ initialWithdrawalGroupId: "",
+ initialWithdrawalStarted: false,
+ lastError: undefined,
+ operationStatus: OperationStatus.Pending,
+ retryInfo: undefined,
+ bankInfo: undefined,
+ restrictAge: undefined,
+ senderWire: undefined,
+ };
+ await tx.reserves.put(rec);
+ return rec;
+ });
+
+ if (!reserve) {
+ throw Error("can't create reserve");
+ }
+
+ const mergeTimestamp = TalerProtocolTimestamp.now();
+
+ const reservePayto = talerPaytoFromExchangeReserve(
+ reserve.exchangeBaseUrl,
+ reserve.reservePub,
+ );
+
+ const sigRes = await ws.cryptoApi.signPurseMerge({
+ contractTermsHash: ContractTermsUtil.hashContractTerms(
+ peerInc.contractTerms,
+ ),
+ flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
+ mergePriv: peerInc.mergePriv,
+ mergeTimestamp: mergeTimestamp,
+ purseAmount: Amounts.stringify(amount),
+ purseExpiration: peerInc.contractTerms.purse_expiration,
+ purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
+ pursePub: peerInc.pursePub,
+ reservePayto,
+ reservePriv: reserve.reservePriv,
+ });
+
+ const mergePurseUrl = new URL(
+ `purses/${req.pursePub}/merge`,
+ req.exchangeBaseUrl,
+ );
+
+ const mergeReq: ExchangePurseMergeRequest = {
+ payto_uri: reservePayto,
+ merge_timestamp: mergeTimestamp,
+ merge_sig: sigRes.mergeSig,
+ reserve_sig: sigRes.accountSig,
+ };
+
+ const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
+
+ const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
+ logger.info(`merge result: ${j2s(res)}`);
}