diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts | 253 |
1 files changed, 177 insertions, 76 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts index 2be21c68d..280ad567f 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -29,6 +29,7 @@ import { TalerError, TalerErrorCode, TalerPreciseTimestamp, + TalerProtocolViolationError, TransactionAction, TransactionMajorState, TransactionMinorState, @@ -44,7 +45,11 @@ import { j2s, parsePayPullUri, } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + HttpResponse, + readSuccessResponseJsonOrThrow, + readTalerErrorResponse, +} from "@gnu-taler/taler-util/http"; import { InternalWalletState, PeerPullDebitRecordStatus, @@ -62,6 +67,7 @@ import { } from "../util/retries.js"; import { runOperationWithErrorReporting, spendCoins } from "./common.js"; import { + PeerCoinRepair, codecForExchangePurseStatus, getTotalPeerPaymentCost, queryCoinInfosForSelection, @@ -76,6 +82,84 @@ import { checkLogicInvariant } from "../util/invariants.js"; const logger = new Logger("pay-peer-pull-debit.ts"); +async function handlePurseCreationConflict( + ws: InternalWalletState, + peerPullInc: PeerPullPaymentIncomingRecord, + resp: HttpResponse, +): Promise<OperationAttemptResult> { + const pursePub = peerPullInc.pursePub; + const errResp = await readTalerErrorResponse(resp); + if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { + await failPeerPullDebitTransaction(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( + peerPullInc.contractTerms.amount, + ); + + const sel = peerPullInc.coinSel; + if (!sel) { + throw Error("invalid state (coin selection expected)"); + } + + const repair: PeerCoinRepair = { + coinPubs: sel.coinPubs, + contribs: sel.contributions.map((x) => Amounts.parseOrThrow(x)), + exchangeBaseUrl: peerPullInc.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", + ); + } + + const totalAmount = await getTotalPeerPaymentCost( + ws, + coinSelRes.result.coins, + ); + + await ws.db + .mktx((x) => [x.peerPullPaymentIncoming]) + .runReadWrite(async (tx) => { + const myPpi = await tx.peerPullPaymentIncoming.get( + peerPullInc.peerPullPaymentIncomingId, + ); + if (!myPpi) { + return; + } + switch (myPpi.status) { + case PeerPullDebitRecordStatus.PendingDeposit: + case PeerPullDebitRecordStatus.SuspendedDeposit: { + const sel = coinSelRes.result; + myPpi.coinSel = { + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => x.contribution), + totalCost: Amounts.stringify(totalAmount), + }; + break; + } + default: + return; + } + await tx.peerPullPaymentIncoming.put(myPpi); + }); + return OperationAttemptResult.finishedEmpty(); +} + async function processPeerPullDebitPendingDeposit( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, @@ -118,81 +202,98 @@ async function processPeerPullDebitPendingDeposit( method: "POST", body: depositPayload, }); - if (httpResp.status === HttpStatusCode.Gone) { - const transitionInfo = await ws.db - .mktx((x) => [ - x.peerPullPaymentIncoming, - x.refreshGroups, - x.denominations, - x.coinAvailability, - x.coins, - ]) - .runReadWrite(async (tx) => { - const pi = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); - if (!pi) { - throw Error("peer pull payment not found anymore"); - } - if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { - return; - } - const oldTxState = computePeerPullDebitTransactionState(pi); - - const currency = Amounts.currencyOf(pi.totalCostEstimated); - const coinPubs: CoinRefreshRequest[] = []; - - if (!pi.coinSel) { - throw Error("invalid db state"); - } - - for (let i = 0; i < pi.coinSel.coinPubs.length; i++) { - coinPubs.push({ - amount: pi.coinSel.contributions[i], - coinPub: pi.coinSel.coinPubs[i], - }); - } - - const refresh = await createRefreshGroup( - ws, - tx, - currency, - coinPubs, - RefreshReason.AbortPeerPushDebit, - ); - - pi.status = PeerPullDebitRecordStatus.AbortingRefresh; - pi.abortRefreshGroupId = refresh.refreshGroupId; - const newTxState = computePeerPullDebitTransactionState(pi); - await tx.peerPullPaymentIncoming.put(pi); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - } else { - const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); - logger.trace(`purse deposit response: ${j2s(resp)}`); - - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadWrite(async (tx) => { - const pi = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); - if (!pi) { - throw Error("peer pull payment not found anymore"); - } - if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { - return; - } - const oldTxState = computePeerPullDebitTransactionState(pi); - pi.status = PeerPullDebitRecordStatus.DonePaid; - const newTxState = computePeerPullDebitTransactionState(pi); - await tx.peerPullPaymentIncoming.put(pi); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); + switch (httpResp.status) { + case HttpStatusCode.Ok: { + const resp = await readSuccessResponseJsonOrThrow( + httpResp, + codecForAny(), + ); + logger.trace(`purse deposit response: ${j2s(resp)}`); + + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentIncoming]) + .runReadWrite(async (tx) => { + const pi = await tx.peerPullPaymentIncoming.get( + peerPullPaymentIncomingId, + ); + if (!pi) { + throw Error("peer pull payment not found anymore"); + } + if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { + return; + } + const oldTxState = computePeerPullDebitTransactionState(pi); + pi.status = PeerPullDebitRecordStatus.DonePaid; + const newTxState = computePeerPullDebitTransactionState(pi); + await tx.peerPullPaymentIncoming.put(pi); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + break; + } + case HttpStatusCode.Gone: { + const transitionInfo = await ws.db + .mktx((x) => [ + x.peerPullPaymentIncoming, + x.refreshGroups, + x.denominations, + x.coinAvailability, + x.coins, + ]) + .runReadWrite(async (tx) => { + const pi = await tx.peerPullPaymentIncoming.get( + peerPullPaymentIncomingId, + ); + if (!pi) { + throw Error("peer pull payment not found anymore"); + } + if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { + return; + } + const oldTxState = computePeerPullDebitTransactionState(pi); + + const currency = Amounts.currencyOf(pi.totalCostEstimated); + const coinPubs: CoinRefreshRequest[] = []; + + if (!pi.coinSel) { + throw Error("invalid db state"); + } + + for (let i = 0; i < pi.coinSel.coinPubs.length; i++) { + coinPubs.push({ + amount: pi.coinSel.contributions[i], + coinPub: pi.coinSel.coinPubs[i], + }); + } + + const refresh = await createRefreshGroup( + ws, + tx, + currency, + coinPubs, + RefreshReason.AbortPeerPushDebit, + ); + + pi.status = PeerPullDebitRecordStatus.AbortingRefresh; + pi.abortRefreshGroupId = refresh.refreshGroupId; + const newTxState = computePeerPullDebitTransactionState(pi); + await tx.peerPullPaymentIncoming.put(pi); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + break; + } + case HttpStatusCode.Conflict: { + return handlePurseCreationConflict(ws, peerPullInc, httpResp); + } + default: { + const errResp = await readTalerErrorResponse(httpResp); + return { + type: OperationAttemptResultType.Error, + errorDetail: errResp, + }; + } } - return { type: OperationAttemptResultType.Finished, result: undefined, @@ -434,7 +535,7 @@ export async function preparePeerPullDebit( const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl); - const purseHttpResp = await ws.http.get(getPurseUrl.href); + const purseHttpResp = await ws.http.fetch(getPurseUrl.href); const purseStatus = await readSuccessResponseJsonOrThrow( purseHttpResp, |