From bcff03949b40d0d37069bdb7af941061e367a093 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 19 Jun 2023 12:02:43 +0200 Subject: wallet-core: implement coin selection repair for p2p payments --- .../src/operations/pay-peer-push-debit.ts | 104 ++++++++++++++++++++- 1 file changed, 99 insertions(+), 5 deletions(-) (limited to 'packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts') diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts index 2835a1f64..33d317c6f 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -28,6 +28,7 @@ import { TalerError, TalerErrorCode, TalerPreciseTimestamp, + TalerProtocolViolationError, TalerUriAction, TransactionAction, TransactionMajorState, @@ -47,8 +48,13 @@ import { getTotalPeerPaymentCost, codecForExchangePurseStatus, queryCoinInfosForSelection, + PeerCoinRepair, } from "./pay-peer-common.js"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + HttpResponse, + readSuccessResponseJsonOrThrow, + readTalerErrorResponse, +} from "@gnu-taler/taler-util/http"; import { PeerPushPaymentInitiationRecord, PeerPushPaymentInitiationStatus, @@ -97,6 +103,73 @@ export async function checkPeerPushDebit( }; } +async function handlePurseCreationConflict( + ws: InternalWalletState, + peerPushInitiation: PeerPushPaymentInitiationRecord, + resp: HttpResponse, +): Promise { + const pursePub = peerPushInitiation.pursePub; + const errResp = await readTalerErrorResponse(resp); + if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { + await failPeerPushDebitTransaction(ws, pursePub); + return OperationAttemptResult.finishedEmpty(); + } + + // FIXME: Properly parse! + const brokenCoinPub = (errResp as any).coin_pub; + logger.trace(`excluded broken coin pub=${brokenCoinPub}`); + + if (!brokenCoinPub) { + // FIXME: Details! + throw new TalerProtocolViolationError(); + } + + const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount); + + const repair: PeerCoinRepair = { + coinPubs: peerPushInitiation.coinSel.coinPubs, + contribs: peerPushInitiation.coinSel.contributions.map((x) => + Amounts.parseOrThrow(x), + ), + exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, + }; + + const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair }); + + if (coinSelRes.type == "failure") { + // FIXME: Details! + throw Error( + "insufficient balance to re-select coins to repair double spending", + ); + } + + await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const myPpi = await tx.peerPushPaymentInitiations.get( + peerPushInitiation.pursePub, + ); + if (!myPpi) { + return; + } + switch (myPpi.status) { + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: { + const sel = coinSelRes.result; + myPpi.coinSel = { + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => x.contribution), + } + break; + } + default: + return; + } + await tx.peerPushPaymentInitiations.put(myPpi); + }); + return OperationAttemptResult.finishedEmpty(); +} + async function processPeerPushDebitCreateReserve( ws: InternalWalletState, peerPushInitiation: PeerPushPaymentInitiationRecord, @@ -175,6 +248,27 @@ async function processPeerPushDebitCreateReserve( logger.info(`resp: ${j2s(resp)}`); + switch (httpResp.status) { + case HttpStatusCode.Ok: + break; + case HttpStatusCode.Forbidden: { + // FIXME: Store this error! + await failPeerPushDebitTransaction(ws, pursePub); + return OperationAttemptResult.finishedEmpty(); + } + case HttpStatusCode.Conflict: { + // Handle double-spending + return handlePurseCreationConflict(ws, peerPushInitiation, resp); + } + default: { + const errResp = await readTalerErrorResponse(resp); + return { + type: OperationAttemptResultType.Error, + errorDetail: errResp, + }; + } + } + if (httpResp.status !== HttpStatusCode.Ok) { // FIXME: do proper error reporting throw Error("got error response from exchange"); @@ -710,17 +804,17 @@ export async function failPeerPushDebitTransaction( switch (pushDebitRec.status) { case PeerPushPaymentInitiationStatus.AbortingRefresh: case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: - // FIXME: We also need to abort the refresh group! - newStatus = PeerPushPaymentInitiationStatus.Aborted; + // FIXME: What to do about the refresh group? + newStatus = PeerPushPaymentInitiationStatus.Failed; break; case PeerPushPaymentInitiationStatus.AbortingDeletePurse: case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPushPaymentInitiationStatus.Aborted; - break; case PeerPushPaymentInitiationStatus.PendingReady: case PeerPushPaymentInitiationStatus.SuspendedReady: case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: case PeerPushPaymentInitiationStatus.PendingCreatePurse: + newStatus = PeerPushPaymentInitiationStatus.Failed; + break; case PeerPushPaymentInitiationStatus.Done: case PeerPushPaymentInitiationStatus.Aborted: case PeerPushPaymentInitiationStatus.Failed: -- cgit v1.2.3