From 24694eae736763ea6e026c8839b7ba119db10bb4 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 12 Jan 2023 15:11:32 +0100 Subject: wallet-core: implement retries for peer push payments --- .../taler-wallet-core/src/operations/pay-peer.ts | 213 +++++++++++++++------ 1 file changed, 159 insertions(+), 54 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index 3ee1795b0..670b547ae 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -68,9 +68,11 @@ import { UnblindedSignature, WalletAccountMergeFlags, } from "@gnu-taler/taler-util"; +import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; import { OperationStatus, PeerPullPaymentIncomingStatus, + PeerPushPaymentCoinSelection, PeerPushPaymentIncomingRecord, PeerPushPaymentInitiationStatus, ReserveRecord, @@ -80,17 +82,26 @@ import { } from "../db.js"; import { TalerError } from "../errors.js"; import { InternalWalletState } from "../internal-wallet-state.js"; -import { makeTransactionId, spendCoins } from "../operations/common.js"; +import { + makeTransactionId, + runOperationWithErrorReporting, + spendCoins, +} from "../operations/common.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { checkDbInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess } from "../util/query.js"; +import { + OperationAttemptResult, + OperationAttemptResultType, + RetryTags, +} from "../util/retries.js"; import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js"; const logger = new Logger("operations/peer-to-peer.ts"); -export interface PeerCoinSelection { +export interface PeerCoinSelectionDetails { exchangeBaseUrl: string; /** @@ -111,6 +122,9 @@ export interface PeerCoinSelection { depositFees: AmountJson; } +/** + * Information about a selected coin for peer to peer payments. + */ interface CoinInfo { /** * Public key of the coin. @@ -131,16 +145,52 @@ interface CoinInfo { denomSig: UnblindedSignature; maxAge: number; + ageCommitmentProof?: AgeCommitmentProof; } export type SelectPeerCoinsResult = - | { type: "success"; result: PeerCoinSelection } + | { type: "success"; result: PeerCoinSelectionDetails } | { type: "failure"; insufficientBalanceDetails: PayPeerInsufficientBalanceDetails; }; +export async function queryCoinInfosForSelection( + ws: InternalWalletState, + csel: PeerPushPaymentCoinSelection, +): Promise { + let infos: SpendCoinDetails[] = []; + await ws.db + .mktx((x) => [x.coins, x.denominations]) + .runReadOnly(async (tx) => { + for (let i = 0; i < csel.coinPubs.length; i++) { + const coin = await tx.coins.get(csel.coinPubs[i]); + if (!coin) { + throw Error("coin not found anymore"); + } + const denom = await ws.getDenomInfo( + ws, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + if (!denom) { + throw Error("denom for coin not found anymore"); + } + infos.push({ + coinPriv: coin.coinPriv, + coinPub: coin.coinPub, + denomPubHash: coin.denomPubHash, + denomSig: coin.denomSig, + ageCommitmentProof: coin.ageCommitmentProof, + contribution: csel.contributions[i], + }); + } + }); + return infos; +} + export async function selectPeerCoins( ws: InternalWalletState, tx: GetReadOnlyAccess<{ @@ -228,7 +278,7 @@ export async function selectPeerCoins( lastDepositFee = coin.feeDeposit; } if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { - const res: PeerCoinSelection = { + const res: PeerCoinSelectionDetails = { exchangeBaseUrl: exch.baseUrl, coins: resCoins, depositFees: depositFeesAcc, @@ -290,6 +340,94 @@ export async function preparePeerPushPayment( }; } +export async function processPeerPushOutgoing( + ws: InternalWalletState, + pursePub: string, +): Promise { + const peerPushInitiation = await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadOnly(async (tx) => { + return tx.peerPushPaymentInitiations.get(pursePub); + }); + if (!peerPushInitiation) { + throw Error("peer push payment not found"); + } + + const purseExpiration = peerPushInitiation.purseExpiration; + const hContractTerms = peerPushInitiation.contractTermsHash; + + const purseSigResp = await ws.cryptoApi.signPurseCreation({ + hContractTerms, + mergePub: peerPushInitiation.mergePub, + minAge: 0, + purseAmount: peerPushInitiation.amount, + purseExpiration, + pursePriv: peerPushInitiation.pursePriv, + }); + + const coins = await queryCoinInfosForSelection( + ws, + peerPushInitiation.coinSel, + ); + + const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ + exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, + pursePub: peerPushInitiation.pursePub, + coins, + }); + + const econtractResp = await ws.cryptoApi.encryptContractForMerge({ + contractTerms: peerPushInitiation.contractTerms, + mergePriv: peerPushInitiation.mergePriv, + pursePriv: peerPushInitiation.pursePriv, + pursePub: peerPushInitiation.pursePub, + contractPriv: peerPushInitiation.contractPriv, + contractPub: peerPushInitiation.contractPub, + }); + + const createPurseUrl = new URL( + `purses/${peerPushInitiation.pursePub}/create`, + peerPushInitiation.exchangeBaseUrl, + ); + + const httpResp = await ws.http.postJson(createPurseUrl.href, { + amount: peerPushInitiation.amount, + merge_pub: peerPushInitiation.mergePub, + purse_sig: purseSigResp.sig, + h_contract_terms: hContractTerms, + purse_expiration: purseExpiration, + deposits: depositSigsResp.deposits, + min_age: 0, + econtract: econtractResp.econtract, + }); + + const resp = await httpResp.json(); + + logger.info(`resp: ${j2s(resp)}`); + + if (httpResp.status !== 200) { + throw Error("got error response from exchange"); + } + + await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const ppi = await tx.peerPushPaymentInitiations.get(pursePub); + if (!ppi) { + return; + } + ppi.status = PeerPushPaymentInitiationStatus.PurseCreated; + }); + + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; +} + +/** + * Initiate sending a peer-to-peer push payment. + */ export async function initiatePeerToPeerPush( ws: InternalWalletState, req: InitiatePeerPushPaymentRequest, @@ -305,13 +443,7 @@ export async function initiatePeerToPeerPush( const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - const econtractResp = await ws.cryptoApi.encryptContractForMerge({ - contractTerms, - mergePriv: mergePair.priv, - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - }); - + const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({}); const coinSelRes: SelectPeerCoinsResult = await ws.db .mktx((x) => [ x.exchanges, @@ -320,7 +452,6 @@ export async function initiatePeerToPeerPush( x.coinAvailability, x.denominations, x.refreshGroups, - x.peerPullPaymentInitiations, x.peerPushPaymentInitiations, ]) .runReadWrite(async (tx) => { @@ -342,7 +473,8 @@ export async function initiatePeerToPeerPush( await tx.peerPushPaymentInitiations.add({ amount: Amounts.stringify(instructedAmount), - contractPriv: econtractResp.contractPriv, + contractPriv: contractKeyPair.priv, + contractPub: contractKeyPair.pub, contractTermsHash: hContractTerms, exchangeBaseUrl: sel.exchangeBaseUrl, mergePriv: mergePair.priv, @@ -351,8 +483,12 @@ export async function initiatePeerToPeerPush( pursePriv: pursePair.priv, pursePub: pursePair.pub, timestampCreated: TalerProtocolTimestamp.now(), - // FIXME: Only set the later when the purse is actually created! - status: PeerPushPaymentInitiationStatus.PurseCreated, + status: PeerPushPaymentInitiationStatus.Initiated, + contractTerms: contractTerms, + coinSel: { + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => x.contribution), + }, }); await tx.contractTerms.put({ @@ -373,53 +509,22 @@ export async function initiatePeerToPeerPush( ); } - const purseSigResp = await ws.cryptoApi.signPurseCreation({ - hContractTerms, - mergePub: mergePair.pub, - minAge: 0, - purseAmount: Amounts.stringify(instructedAmount), - purseExpiration, - pursePriv: pursePair.priv, - }); - - const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ - exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, - pursePub: pursePair.pub, - coins: coinSelRes.result.coins, - }); - - const createPurseUrl = new URL( - `purses/${pursePair.pub}/create`, - coinSelRes.result.exchangeBaseUrl, + await runOperationWithErrorReporting( + ws, + RetryTags.byPeerPushPaymentInitiationPursePub(pursePair.pub), + async () => { + return await processPeerPushOutgoing(ws, pursePair.pub); + }, ); - const httpResp = await ws.http.postJson(createPurseUrl.href, { - amount: Amounts.stringify(instructedAmount), - merge_pub: mergePair.pub, - purse_sig: purseSigResp.sig, - h_contract_terms: hContractTerms, - purse_expiration: purseExpiration, - deposits: depositSigsResp.deposits, - min_age: 0, - econtract: econtractResp.econtract, - }); - - const resp = await httpResp.json(); - - logger.info(`resp: ${j2s(resp)}`); - - if (httpResp.status !== 200) { - throw Error("got error response from exchange"); - } - return { - contractPriv: econtractResp.contractPriv, + contractPriv: contractKeyPair.priv, mergePriv: mergePair.priv, pursePub: pursePair.pub, exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, talerUri: constructPayPushUri({ exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, - contractPriv: econtractResp.contractPriv, + contractPriv: contractKeyPair.priv, }), transactionId: makeTransactionId( TransactionType.PeerPushDebit, -- cgit v1.2.3