/* This file is part of GNU Taler (C) 2022-2023 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { AcceptPeerPullPaymentResponse, Amounts, CoinRefreshRequest, ConfirmPeerPullDebitRequest, ContractTermsUtil, ExchangePurseDeposits, HttpStatusCode, Logger, NotificationType, PeerContractTerms, PreparePeerPullDebitRequest, PreparePeerPullDebitResponse, RefreshReason, TalerError, TalerErrorCode, TalerPreciseTimestamp, TalerProtocolViolationError, TransactionAction, TransactionMajorState, TransactionMinorState, TransactionState, TransactionType, codecForAny, codecForExchangeGetContractResponse, codecForPeerContractTerms, decodeCrock, eddsaGetPublic, encodeCrock, getRandomBytes, j2s, parsePayPullUri, } from "@gnu-taler/taler-util"; import { HttpResponse, readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; import { InternalWalletState, PeerPullDebitRecordStatus, PeerPullPaymentIncomingRecord, PendingTaskType, RefreshOperationStatus, createRefreshGroup, timestampPreciseToDb, } from "../index.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { TaskRunResult, TaskRunResultType, constructTaskIdentifier, spendCoins, } from "./common.js"; import { codecForExchangePurseStatus, getTotalPeerPaymentCost, queryCoinInfosForSelection, } from "./pay-peer-common.js"; import { constructTransactionIdentifier, notifyTransition, parseTransactionIdentifier, stopLongpolling, } from "./transactions.js"; import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js"; const logger = new Logger("pay-peer-pull-debit.ts"); async function handlePurseCreationConflict( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, resp: HttpResponse, ): Promise { const pursePub = peerPullInc.pursePub; const errResp = await readTalerErrorResponse(resp); if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { await failPeerPullDebitTransaction(ws, pursePub); return TaskRunResult.finished(); } // 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.amount); const sel = peerPullInc.coinSel; if (!sel) { throw Error("invalid state (coin selection expected)"); } const repair: PeerCoinRepair = { coinPubs: [], contribs: [], exchangeBaseUrl: peerPullInc.exchangeBaseUrl, }; for (let i = 0; i < sel.coinPubs.length; i++) { if (sel.coinPubs[i] != brokenCoinPub) { repair.coinPubs.push(sel.coinPubs[i]); repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i])); } } 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.peerPullDebit]) .runReadWrite(async (tx) => { const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId); 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.peerPullDebit.put(myPpi); }); return TaskRunResult.finished(); } async function processPeerPullDebitPendingDeposit( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, ): Promise { const peerPullDebitId = peerPullInc.peerPullDebitId; const pursePub = peerPullInc.pursePub; const coinSel = peerPullInc.coinSel; if (!coinSel) { throw Error("invalid state, no coins selected"); } const coins = await queryCoinInfosForSelection(ws, coinSel); const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ exchangeBaseUrl: peerPullInc.exchangeBaseUrl, pursePub: peerPullInc.pursePub, coins, }); const purseDepositUrl = new URL( `purses/${pursePub}/deposit`, peerPullInc.exchangeBaseUrl, ); const depositPayload: ExchangePurseDeposits = { deposits: depositSigsResp.deposits, }; if (logger.shouldLogTrace()) { logger.trace(`purse deposit payload: ${j2s(depositPayload)}`); } const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); const httpResp = await ws.http.fetch(purseDepositUrl.href, { method: "POST", body: depositPayload, }); 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.peerPullDebit]) .runReadWrite(async (tx) => { const pi = await tx.peerPullDebit.get(peerPullDebitId); if (!pi) { throw Error("peer pull payment not found anymore"); } if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { return; } const oldTxState = computePeerPullDebitTransactionState(pi); pi.status = PeerPullDebitRecordStatus.Done; const newTxState = computePeerPullDebitTransactionState(pi); await tx.peerPullDebit.put(pi); return { oldTxState, newTxState }; }); notifyTransition(ws, transactionId, transitionInfo); break; } case HttpStatusCode.Gone: { const transitionInfo = await ws.db .mktx((x) => [ x.peerPullDebit, x.refreshGroups, x.denominations, x.coinAvailability, x.coins, ]) .runReadWrite(async (tx) => { const pi = await tx.peerPullDebit.get(peerPullDebitId); 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.AbortPeerPullDebit, ); pi.status = PeerPullDebitRecordStatus.AbortingRefresh; pi.abortRefreshGroupId = refresh.refreshGroupId; const newTxState = computePeerPullDebitTransactionState(pi); await tx.peerPullDebit.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: TaskRunResultType.Error, errorDetail: errResp, }; } } return TaskRunResult.finished(); } async function processPeerPullDebitAbortingRefresh( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, ): Promise { const peerPullDebitId = peerPullInc.peerPullDebitId; const abortRefreshGroupId = peerPullInc.abortRefreshGroupId; checkLogicInvariant(!!abortRefreshGroupId); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); const transitionInfo = await ws.db .mktx((x) => [x.refreshGroups, x.peerPullDebit]) .runReadWrite(async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); let newOpState: PeerPullDebitRecordStatus | undefined; if (!refreshGroup) { // Maybe it got manually deleted? Means that we should // just go into failed. logger.warn("no aborting refresh group found for deposit group"); newOpState = PeerPullDebitRecordStatus.Failed; } else { if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) { newOpState = PeerPullDebitRecordStatus.Aborted; } else if ( refreshGroup.operationStatus === RefreshOperationStatus.Failed ) { newOpState = PeerPullDebitRecordStatus.Failed; } } if (newOpState) { const newDg = await tx.peerPullDebit.get(peerPullDebitId); if (!newDg) { return; } const oldTxState = computePeerPullDebitTransactionState(newDg); newDg.status = newOpState; const newTxState = computePeerPullDebitTransactionState(newDg); await tx.peerPullDebit.put(newDg); return { oldTxState, newTxState }; } return undefined; }); notifyTransition(ws, transactionId, transitionInfo); // FIXME: Shouldn't this be finished in some cases?! return TaskRunResult.pending(); } export async function processPeerPullDebit( ws: InternalWalletState, peerPullDebitId: string, ): Promise { const peerPullInc = await ws.db .mktx((x) => [x.peerPullDebit]) .runReadOnly(async (tx) => { return tx.peerPullDebit.get(peerPullDebitId); }); if (!peerPullInc) { throw Error("peer pull debit not found"); } switch (peerPullInc.status) { case PeerPullDebitRecordStatus.PendingDeposit: return await processPeerPullDebitPendingDeposit(ws, peerPullInc); case PeerPullDebitRecordStatus.AbortingRefresh: return await processPeerPullDebitAbortingRefresh(ws, peerPullInc); } return TaskRunResult.finished(); } export async function confirmPeerPullDebit( ws: InternalWalletState, req: ConfirmPeerPullDebitRequest, ): Promise { let peerPullDebitId: string; if (req.transactionId) { const parsedTx = parseTransactionIdentifier(req.transactionId); if (!parsedTx || parsedTx.tag !== TransactionType.PeerPullDebit) { throw Error("invalid peer-pull-debit transaction identifier"); } peerPullDebitId = parsedTx.peerPullDebitId; } else if (req.peerPullDebitId) { peerPullDebitId = req.peerPullDebitId; } else { throw Error("invalid request, transactionId or peerPullDebitId required"); } const peerPullInc = await ws.db .mktx((x) => [x.peerPullDebit]) .runReadOnly(async (tx) => { return tx.peerPullDebit.get(peerPullDebitId); }); if (!peerPullInc) { throw Error( `can't accept unknown incoming p2p pull payment (${req.peerPullDebitId})`, ); } const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount); const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); if (logger.shouldLogTrace()) { logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); } if (coinSelRes.type !== "success") { throw TalerError.fromDetail( TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, { insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, }, ); } const sel = coinSelRes.result; const totalAmount = await getTotalPeerPaymentCost( ws, coinSelRes.result.coins, ); const ppi = await ws.db .mktx((x) => [ x.exchanges, x.coins, x.denominations, x.refreshGroups, x.peerPullDebit, x.coinAvailability, ]) .runReadWrite(async (tx) => { await spendCoins(ws, tx, { // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`, allocationId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }), coinPubs: sel.coins.map((x) => x.coinPub), contributions: sel.coins.map((x) => Amounts.parseOrThrow(x.contribution), ), refreshReason: RefreshReason.PayPeerPull, }); const pi = await tx.peerPullDebit.get(peerPullDebitId); if (!pi) { throw Error(); } if (pi.status === PeerPullDebitRecordStatus.DialogProposed) { pi.status = PeerPullDebitRecordStatus.PendingDeposit; pi.coinSel = { coinPubs: sel.coins.map((x) => x.coinPub), contributions: sel.coins.map((x) => x.contribution), totalCost: Amounts.stringify(totalAmount), }; } await tx.peerPullDebit.put(pi); return pi; }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); ws.notify({ type: NotificationType.BalanceChange, hintTransactionId: transactionId, }); return { transactionId, }; } /** * Look up information about an incoming peer pull payment. * Store the results in the wallet DB. */ export async function preparePeerPullDebit( ws: InternalWalletState, req: PreparePeerPullDebitRequest, ): Promise { const uri = parsePayPullUri(req.talerUri); if (!uri) { throw Error("got invalid taler://pay-pull URI"); } const existing = await ws.db .mktx((x) => [x.peerPullDebit, x.contractTerms]) .runReadOnly(async (tx) => { const peerPullDebitRecord = await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([ uri.exchangeBaseUrl, uri.contractPriv, ]); if (!peerPullDebitRecord) { return; } const contractTerms = await tx.contractTerms.get( peerPullDebitRecord.contractTermsHash, ); if (!contractTerms) { return; } return { peerPullDebitRecord, contractTerms }; }); if (existing) { return { amount: existing.peerPullDebitRecord.amount, amountRaw: existing.peerPullDebitRecord.amount, amountEffective: existing.peerPullDebitRecord.totalCostEstimated, contractTerms: existing.contractTerms.contractTermsRaw, peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId, }), }; } const exchangeBaseUrl = uri.exchangeBaseUrl; const contractPriv = uri.contractPriv; const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); const contractHttpResp = await ws.http.fetch(getContractUrl.href); const contractResp = await readSuccessResponseJsonOrThrow( contractHttpResp, codecForExchangeGetContractResponse(), ); const pursePub = contractResp.purse_pub; const dec = await ws.cryptoApi.decryptContractForDeposit({ ciphertext: contractResp.econtract, contractPriv: contractPriv, pursePub: pursePub, }); const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl); const purseHttpResp = await ws.http.fetch(getPurseUrl.href); const purseStatus = await readSuccessResponseJsonOrThrow( purseHttpResp, codecForExchangePurseStatus(), ); const peerPullDebitId = encodeCrock(getRandomBytes(32)); let contractTerms: PeerContractTerms; if (dec.contractTerms) { contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); // FIXME: Check that the purseStatus balance matches contract terms amount } else { // FIXME: In this case, where do we get the purse expiration from?! // https://bugs.gnunet.org/view.php?id=7706 throw Error("pull payments without contract terms not supported yet"); } const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms); // FIXME: Why don't we compute the totalCost here?! const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); if (logger.shouldLogTrace()) { logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); } if (coinSelRes.type !== "success") { throw TalerError.fromDetail( TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, { insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, }, ); } const totalAmount = await getTotalPeerPaymentCost( ws, coinSelRes.result.coins, ); await ws.db .mktx((x) => [x.peerPullDebit, x.contractTerms]) .runReadWrite(async (tx) => { await tx.contractTerms.put({ h: contractTermsHash, contractTermsRaw: contractTerms, }), await tx.peerPullDebit.add({ peerPullDebitId, contractPriv: contractPriv, exchangeBaseUrl: exchangeBaseUrl, pursePub: pursePub, timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), contractTermsHash, amount: contractTerms.amount, status: PeerPullDebitRecordStatus.DialogProposed, totalCostEstimated: Amounts.stringify(totalAmount), }); }); return { amount: contractTerms.amount, amountEffective: Amounts.stringify(totalAmount), amountRaw: contractTerms.amount, contractTerms: contractTerms, peerPullDebitId, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId: peerPullDebitId, }), }; } export async function suspendPeerPullDebitTransaction( ws: InternalWalletState, peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); if (!pullDebitRec) { logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; switch (pullDebitRec.status) { case PeerPullDebitRecordStatus.DialogProposed: break; case PeerPullDebitRecordStatus.Done: break; case PeerPullDebitRecordStatus.PendingDeposit: newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; break; case PeerPullDebitRecordStatus.SuspendedDeposit: break; case PeerPullDebitRecordStatus.Aborted: break; case PeerPullDebitRecordStatus.AbortingRefresh: newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh; break; case PeerPullDebitRecordStatus.Failed: break; case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: break; default: assertUnreachable(pullDebitRec.status); } if (newStatus != null) { const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, }; } return undefined; }); notifyTransition(ws, transactionId, transitionInfo); } export async function abortPeerPullDebitTransaction( ws: InternalWalletState, peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); if (!pullDebitRec) { logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; switch (pullDebitRec.status) { case PeerPullDebitRecordStatus.DialogProposed: newStatus = PeerPullDebitRecordStatus.Aborted; break; case PeerPullDebitRecordStatus.Done: break; case PeerPullDebitRecordStatus.PendingDeposit: newStatus = PeerPullDebitRecordStatus.AbortingRefresh; break; case PeerPullDebitRecordStatus.SuspendedDeposit: break; case PeerPullDebitRecordStatus.Aborted: break; case PeerPullDebitRecordStatus.AbortingRefresh: break; case PeerPullDebitRecordStatus.Failed: break; case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: break; default: assertUnreachable(pullDebitRec.status); } if (newStatus != null) { const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, }; } return undefined; }); notifyTransition(ws, transactionId, transitionInfo); } export async function failPeerPullDebitTransaction( ws: InternalWalletState, peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); if (!pullDebitRec) { logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; switch (pullDebitRec.status) { case PeerPullDebitRecordStatus.DialogProposed: newStatus = PeerPullDebitRecordStatus.Aborted; break; case PeerPullDebitRecordStatus.Done: break; case PeerPullDebitRecordStatus.PendingDeposit: break; case PeerPullDebitRecordStatus.SuspendedDeposit: break; case PeerPullDebitRecordStatus.Aborted: break; case PeerPullDebitRecordStatus.Failed: break; case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: case PeerPullDebitRecordStatus.AbortingRefresh: // FIXME: abort underlying refresh! newStatus = PeerPullDebitRecordStatus.Failed; break; default: assertUnreachable(pullDebitRec.status); } if (newStatus != null) { const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, }; } return undefined; }); notifyTransition(ws, transactionId, transitionInfo); } export async function resumePeerPullDebitTransaction( ws: InternalWalletState, peerPullDebitId: string, ) { const taskId = constructTaskIdentifier({ tag: PendingTaskType.PeerPullDebit, peerPullDebitId, }); const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId, }); stopLongpolling(ws, taskId); const transitionInfo = await ws.db .mktx((x) => [x.peerPullDebit]) .runReadWrite(async (tx) => { const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); if (!pullDebitRec) { logger.warn(`peer pull debit ${peerPullDebitId} not found`); return; } let newStatus: PeerPullDebitRecordStatus | undefined = undefined; switch (pullDebitRec.status) { case PeerPullDebitRecordStatus.DialogProposed: case PeerPullDebitRecordStatus.Done: case PeerPullDebitRecordStatus.PendingDeposit: break; case PeerPullDebitRecordStatus.SuspendedDeposit: newStatus = PeerPullDebitRecordStatus.PendingDeposit; break; case PeerPullDebitRecordStatus.Aborted: break; case PeerPullDebitRecordStatus.AbortingRefresh: break; case PeerPullDebitRecordStatus.Failed: break; case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: newStatus = PeerPullDebitRecordStatus.AbortingRefresh; break; default: assertUnreachable(pullDebitRec.status); } if (newStatus != null) { const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); await tx.peerPullDebit.put(pullDebitRec); return { oldTxState, newTxState, }; } return undefined; }); ws.workAvailable.trigger(); notifyTransition(ws, transactionId, transitionInfo); } export function computePeerPullDebitTransactionState( pullDebitRecord: PeerPullPaymentIncomingRecord, ): TransactionState { switch (pullDebitRecord.status) { case PeerPullDebitRecordStatus.DialogProposed: return { major: TransactionMajorState.Dialog, minor: TransactionMinorState.Proposed, }; case PeerPullDebitRecordStatus.PendingDeposit: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.Deposit, }; case PeerPullDebitRecordStatus.Done: return { major: TransactionMajorState.Done, }; case PeerPullDebitRecordStatus.SuspendedDeposit: return { major: TransactionMajorState.Suspended, minor: TransactionMinorState.Deposit, }; case PeerPullDebitRecordStatus.Aborted: return { major: TransactionMajorState.Aborted, }; case PeerPullDebitRecordStatus.AbortingRefresh: return { major: TransactionMajorState.Aborting, minor: TransactionMinorState.Refresh, }; case PeerPullDebitRecordStatus.Failed: return { major: TransactionMajorState.Failed, }; case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: return { major: TransactionMajorState.SuspendedAborting, minor: TransactionMinorState.Refresh, }; } } export function computePeerPullDebitTransactionActions( pullDebitRecord: PeerPullPaymentIncomingRecord, ): TransactionAction[] { switch (pullDebitRecord.status) { case PeerPullDebitRecordStatus.DialogProposed: return []; case PeerPullDebitRecordStatus.PendingDeposit: return [TransactionAction.Abort, TransactionAction.Suspend]; case PeerPullDebitRecordStatus.Done: return [TransactionAction.Delete]; case PeerPullDebitRecordStatus.SuspendedDeposit: return [TransactionAction.Resume, TransactionAction.Abort]; case PeerPullDebitRecordStatus.Aborted: return [TransactionAction.Delete]; case PeerPullDebitRecordStatus.AbortingRefresh: return [TransactionAction.Fail, TransactionAction.Suspend]; case PeerPullDebitRecordStatus.Failed: return [TransactionAction.Delete]; case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: return [TransactionAction.Resume, TransactionAction.Fail]; } }