diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer.ts | 672 |
1 files changed, 587 insertions, 85 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index 33659afe0..31e395cab 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -75,20 +75,21 @@ import { NotificationType, HttpStatusCode, codecForWalletKycUuid, - WalletKycUuid, + TransactionState, + TransactionMajorState, + TransactionMinorState, } from "@gnu-taler/taler-util"; import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; import { DenominationRecord, - KycPendingInfo, - KycUserType, - OperationStatus, + PeerPullPaymentIncomingRecord, PeerPullPaymentIncomingStatus, PeerPullPaymentInitiationRecord, PeerPullPaymentInitiationStatus, PeerPushPaymentCoinSelection, PeerPushPaymentIncomingRecord, PeerPushPaymentIncomingStatus, + PeerPushPaymentInitiationRecord, PeerPushPaymentInitiationStatus, ReserveRecord, WithdrawalGroupStatus, @@ -98,7 +99,6 @@ import { TalerError } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; import { LongpollResult, - makeTransactionId, resetOperationTimeout, runLongpollAsync, runOperationWithErrorReporting, @@ -128,8 +128,10 @@ import { import { PendingTaskType } from "../pending-types.js"; import { constructTransactionIdentifier, + notifyTransition, stopLongpolling, } from "./transactions.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; const logger = new Logger("operations/peer-to-peer.ts"); @@ -451,19 +453,11 @@ export async function checkPeerPushDebit( }; } -export async function processPeerPushInitiation( +async function processPeerPushDebitCreateReserve( ws: InternalWalletState, - pursePub: string, + peerPushInitiation: PeerPushPaymentInitiationRecord, ): Promise<OperationAttemptResult> { - 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 pursePub = peerPushInitiation.pursePub; const purseExpiration = peerPushInitiation.purseExpiration; const hContractTerms = peerPushInitiation.contractTermsHash; @@ -501,22 +495,25 @@ export async function processPeerPushInitiation( 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 httpResp = await ws.http.fetch(createPurseUrl.href, { + method: "POST", + body: { + 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) { + if (httpResp.status !== HttpStatusCode.Ok) { throw Error("got error response from exchange"); } @@ -527,7 +524,7 @@ export async function processPeerPushInitiation( if (!ppi) { return; } - ppi.status = PeerPushPaymentInitiationStatus.PurseCreated; + ppi.status = PeerPushPaymentInitiationStatus.Done; await tx.peerPushPaymentInitiations.put(ppi); }); @@ -537,6 +534,122 @@ export async function processPeerPushInitiation( }; } +async function transitionPeerPushDebitFromReadyToDone( + ws: InternalWalletState, + pursePub: string, +): Promise<void> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub); + if (!ppiRec) { + return undefined; + } + if (ppiRec.status !== PeerPushPaymentInitiationStatus.PendingReady) { + return undefined; + } + const oldTxState = computePeerPushDebitTransactionState(ppiRec); + ppiRec.status = PeerPushPaymentInitiationStatus.Done; + const newTxState = computePeerPushDebitTransactionState(ppiRec); + return { + oldTxState, + newTxState, + }; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +/** + * Process the "pending(ready)" state of a peer-push-debit transaction. + */ +async function processPeerPushDebitReady( + ws: InternalWalletState, + peerPushInitiation: PeerPushPaymentInitiationRecord, +): Promise<OperationAttemptResult> { + const pursePub = peerPushInitiation.pursePub; + const retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + runLongpollAsync(ws, retryTag, async (ct) => { + const mergeUrl = new URL(`purses/${pursePub}/merge`); + mergeUrl.searchParams.set("timeout_ms", "30000"); + const resp = await ws.http.fetch(mergeUrl.href, { + // timeout: getReserveRequestTimeout(withdrawalGroup), + cancellationToken: ct, + }); + if (resp.status === HttpStatusCode.Ok) { + const purseStatus = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangePurseStatus(), + ); + if (purseStatus.deposit_timestamp) { + await transitionPeerPushDebitFromReadyToDone( + ws, + peerPushInitiation.pursePub, + ); + return { + ready: true, + }; + } + } else if (resp.status === HttpStatusCode.Gone) { + // FIXME: transition the reserve into the expired state + } + return { + ready: false, + }; + }); + logger.trace( + "returning early from withdrawal for long-polling in background", + ); + return { + type: OperationAttemptResultType.Longpoll, + }; +} + +export async function processPeerPushDebit( + ws: InternalWalletState, + pursePub: string, +): Promise<OperationAttemptResult> { + 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 retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + + // We're already running! + if (ws.activeLongpoll[retryTag]) { + logger.info("peer-push-debit task already in long-polling, returning!"); + return { + type: OperationAttemptResultType.Longpoll, + }; + } + + switch (peerPushInitiation.status) { + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + return processPeerPushDebitCreateReserve(ws, peerPushInitiation); + case PeerPushPaymentInitiationStatus.PendingReady: + return processPeerPushDebitReady(ws, peerPushInitiation); + } + + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; +} + /** * Initiate sending a peer-to-peer push payment. */ @@ -612,7 +725,7 @@ export async function initiatePeerPushPayment( pursePriv: pursePair.priv, pursePub: pursePair.pub, timestampCreated: TalerProtocolTimestamp.now(), - status: PeerPushPaymentInitiationStatus.Initiated, + status: PeerPushPaymentInitiationStatus.PendingCreatePurse, contractTerms: contractTerms, coinSel: { coinPubs: sel.coins.map((x) => x.coinPub), @@ -628,12 +741,12 @@ export async function initiatePeerPushPayment( }); const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushInitiation, + tag: PendingTaskType.PeerPushDebit, pursePub: pursePair.pub, }); await runOperationWithErrorReporting(ws, taskId, async () => { - return await processPeerPushInitiation(ws, pursePair.pub); + return await processPeerPushDebit(ws, pursePair.pub); }); return { @@ -645,22 +758,24 @@ export async function initiatePeerPushPayment( exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, contractPriv: contractKeyPair.priv, }), - transactionId: makeTransactionId( - TransactionType.PeerPushDebit, - pursePair.pub, - ), + transactionId: constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub: pursePair.pub, + }), }; } interface ExchangePurseStatus { balance: AmountString; deposit_timestamp?: TalerProtocolTimestamp; + merge_timestamp?: TalerProtocolTimestamp; } export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> => buildCodecForObject<ExchangePurseStatus>() .property("balance", codecForAmountString()) .property("deposit_timestamp", codecOptional(codecForTimestamp)) + .property("merge_timestamp", codecOptional(codecForTimestamp)) .build("ExchangePurseStatus"); export async function preparePeerPushCredit( @@ -879,13 +994,13 @@ export async function processPeerPushCredit( const amount = Amounts.parseOrThrow(contractTerms.amount); if ( - peerInc.status === PeerPushPaymentIncomingStatus.KycRequired && + peerInc.status === PeerPushPaymentIncomingStatus.MergeKycRequired && peerInc.kycInfo ) { - const txId = makeTransactionId( - TransactionType.PeerPushCredit, - peerInc.peerPushPaymentIncomingId, - ); + const txId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, + }); await checkWithdrawalKycStatus( ws, peerInc.exchangeBaseUrl, @@ -951,7 +1066,7 @@ export async function processPeerPushCredit( paytoHash: kycPending.h_payto, requirementRow: kycPending.requirement_row, }; - peerInc.status = PeerPushPaymentIncomingStatus.KycRequired; + peerInc.status = PeerPushPaymentIncomingStatus.MergeKycRequired; await tx.peerPushPaymentIncoming.put(peerInc); }); return { @@ -975,7 +1090,7 @@ export async function processPeerPushCredit( }, forcedWithdrawalGroupId: peerInc.withdrawalGroupId, exchangeBaseUrl: peerInc.exchangeBaseUrl, - reserveStatus: WithdrawalGroupStatus.QueryingStatus, + reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, reserveKeyPair: { priv: mergeReserveInfo.reservePriv, pub: mergeReserveInfo.reservePub, @@ -993,9 +1108,9 @@ export async function processPeerPushCredit( } if ( peerInc.status === PeerPushPaymentIncomingStatus.Accepted || - peerInc.status === PeerPushPaymentIncomingStatus.KycRequired + peerInc.status === PeerPushPaymentIncomingStatus.MergeKycRequired ) { - peerInc.status = PeerPushPaymentIncomingStatus.WithdrawalCreated; + peerInc.status = PeerPushPaymentIncomingStatus.Done; } await tx.peerPushPaymentIncoming.put(peerInc); }); @@ -1011,7 +1126,7 @@ export async function confirmPeerPushCredit( req: ConfirmPeerPushCreditRequest, ): Promise<AcceptPeerPushPaymentResponse> { let peerInc: PeerPushPaymentIncomingRecord | undefined; - let contractTerms: PeerContractTerms | undefined; + await ws.db .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) .runReadWrite(async (tx) => { @@ -1021,10 +1136,6 @@ export async function confirmPeerPushCredit( if (!peerInc) { return; } - const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash); - if (ctRec) { - contractTerms = ctRec.contractTermsRaw; - } if (peerInc.status === PeerPushPaymentIncomingStatus.Proposed) { peerInc.status = PeerPushPaymentIncomingStatus.Accepted; } @@ -1037,21 +1148,15 @@ export async function confirmPeerPushCredit( ); } - checkDbInvariant(!!contractTerms); - - await updateExchangeFromUrl(ws, peerInc.exchangeBaseUrl); - - const retryTag = TaskIdentifiers.forPeerPushCredit(peerInc); + ws.workAvailable.trigger(); - await runOperationWithErrorReporting(ws, retryTag, () => - processPeerPushCredit(ws, req.peerPushPaymentIncomingId), - ); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId: req.peerPushPaymentIncomingId, + }); return { - transactionId: makeTransactionId( - TransactionType.PeerPushCredit, - req.peerPushPaymentIncomingId, - ), + transactionId, }; } @@ -1209,11 +1314,13 @@ export async function confirmPeerPullDebit( }, ); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullDebit, + peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, + }); + return { - transactionId: makeTransactionId( - TransactionType.PeerPullDebit, - req.peerPullPaymentIncomingId, - ), + transactionId, }; } @@ -1395,7 +1502,7 @@ export async function queryPurseForPeerPullCredit( }, forcedWithdrawalGroupId: pullIni.withdrawalGroupId, exchangeBaseUrl: pullIni.exchangeBaseUrl, - reserveStatus: WithdrawalGroupStatus.QueryingStatus, + reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, reserveKeyPair: { priv: reserve.reservePriv, pub: reserve.reservePub, @@ -1410,8 +1517,8 @@ export async function queryPurseForPeerPullCredit( logger.warn("peerPullPaymentInitiation not found anymore"); return; } - if (finPi.status === PeerPullPaymentInitiationStatus.PurseCreated) { - finPi.status = PeerPullPaymentInitiationStatus.PurseDeposited; + if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) { + finPi.status = PeerPullPaymentInitiationStatus.DonePurseDeposited; } await tx.peerPullPaymentInitiations.put(finPi); }); @@ -1434,7 +1541,7 @@ export async function processPeerPullCredit( } const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullInitiation, + tag: PendingTaskType.PeerPullCredit, pursePub, }); @@ -1449,7 +1556,7 @@ export async function processPeerPullCredit( logger.trace(`processing ${retryTag}, status=${pullIni.status}`); switch (pullIni.status) { - case PeerPullPaymentInitiationStatus.PurseDeposited: { + case PeerPullPaymentInitiationStatus.DonePurseDeposited: { // We implement this case so that the "retry" action on a peer-pull-credit transaction // also retries the withdrawal task. @@ -1475,7 +1582,7 @@ export async function processPeerPullCredit( result: undefined, }; } - case PeerPullPaymentInitiationStatus.PurseCreated: + case PeerPullPaymentInitiationStatus.PendingReady: runLongpollAsync(ws, retryTag, async (cancellationToken) => queryPurseForPeerPullCredit(ws, pullIni, cancellationToken), ); @@ -1485,23 +1592,23 @@ export async function processPeerPullCredit( return { type: OperationAttemptResultType.Longpoll, }; - case PeerPullPaymentInitiationStatus.KycRequired: { + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub: pullIni.pursePub, + }); if (pullIni.kycInfo) { - const txId = makeTransactionId( - TransactionType.PeerPullCredit, - pullIni.pursePub, - ); await checkWithdrawalKycStatus( ws, pullIni.exchangeBaseUrl, - txId, + transactionId, pullIni.kycInfo, "individual", ); } break; } - case PeerPullPaymentInitiationStatus.Initial: + case PeerPullPaymentInitiationStatus.PendingCreatePurse: break; default: throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`); @@ -1590,7 +1697,8 @@ export async function processPeerPullCredit( paytoHash: kycPending.h_payto, requirementRow: kycPending.requirement_row, }; - peerIni.status = PeerPullPaymentInitiationStatus.KycRequired; + peerIni.status = + PeerPullPaymentInitiationStatus.PendingMergeKycRequired; await tx.peerPullPaymentInitiations.put(peerIni); }); return { @@ -1610,7 +1718,7 @@ export async function processPeerPullCredit( if (!pi2) { return; } - pi2.status = PeerPullPaymentInitiationStatus.PurseCreated; + pi2.status = PeerPullPaymentInitiationStatus.PendingReady; await tx.peerPullPaymentInitiations.put(pi2); }); @@ -1776,7 +1884,7 @@ export async function initiatePeerPullPayment( pursePub: pursePair.pub, mergePriv: mergePair.priv, mergePub: mergePair.pub, - status: PeerPullPaymentInitiationStatus.Initial, + status: PeerPullPaymentInitiationStatus.PendingCreatePurse, contractTerms: contractTerms, mergeTimestamp, mergeReserveRowId: mergeReserveRowId, @@ -1796,7 +1904,7 @@ export async function initiatePeerPullPayment( // check this asynchronously from the transaction status? const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullInitiation, + tag: PendingTaskType.PeerPullCredit, pursePub: pursePair.pub, }); @@ -1804,14 +1912,408 @@ export async function initiatePeerPullPayment( return processPeerPullCredit(ws, pursePair.pub); }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub: pursePair.pub, + }); + return { talerUri: constructPayPullUri({ exchangeBaseUrl: exchangeBaseUrl, contractPriv: contractKeyPair.priv, }), - transactionId: makeTransactionId( - TransactionType.PeerPullCredit, - pursePair.pub, - ), + transactionId, }; } + +export function computePeerPushDebitTransactionState( + ppiRecord: PeerPushPaymentInitiationRecord, +): TransactionState { + switch (ppiRecord.status) { + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.CreatePurse, + }; + case PeerPushPaymentInitiationStatus.PendingReady: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }; + case PeerPushPaymentInitiationStatus.Aborted: + return { + major: TransactionMajorState.Aborted, + }; + case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + return { + major: TransactionMajorState.Aborting, + minor: TransactionMinorState.DeletePurse, + }; + case PeerPushPaymentInitiationStatus.AbortingRefresh: + return { + major: TransactionMajorState.Aborting, + minor: TransactionMinorState.Refresh, + }; + case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + return { + major: TransactionMajorState.SuspendedAborting, + minor: TransactionMinorState.DeletePurse, + }; + case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + return { + major: TransactionMajorState.SuspendedAborting, + minor: TransactionMinorState.Refresh, + }; + case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.CreatePurse, + }; + case PeerPushPaymentInitiationStatus.SuspendedReady: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.Ready, + }; + case PeerPushPaymentInitiationStatus.Done: + return { + major: TransactionMajorState.Done, + }; + } +} + +export async function abortPeerPushDebitTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushPaymentInitiationStatus.PendingReady: + case PeerPushPaymentInitiationStatus.SuspendedReady: + newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; + break; + case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + // Network request might already be in-flight! + newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; + break; + case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPushPaymentInitiationStatus.AbortingRefresh: + case PeerPushPaymentInitiationStatus.Done: + case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + case PeerPushPaymentInitiationStatus.Aborted: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushPaymentInitiations.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function cancelAbortingPeerPushDebitTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushPaymentInitiationStatus.AbortingRefresh: + case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + // FIXME: We also need to abort the refresh group! + newStatus = PeerPushPaymentInitiationStatus.Aborted; + break; + case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPushPaymentInitiationStatus.Aborted; + break; + case PeerPushPaymentInitiationStatus.PendingReady: + case PeerPushPaymentInitiationStatus.SuspendedReady: + case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushPaymentInitiationStatus.Done: + case PeerPushPaymentInitiationStatus.Aborted: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushPaymentInitiations.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function suspendPeerPushDebitTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse; + break; + case PeerPushPaymentInitiationStatus.AbortingRefresh: + newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh; + break; + case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + newStatus = + PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse; + break; + case PeerPushPaymentInitiationStatus.PendingReady: + newStatus = PeerPushPaymentInitiationStatus.SuspendedReady; + break; + case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + case PeerPushPaymentInitiationStatus.SuspendedReady: + case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPushPaymentInitiationStatus.Done: + case PeerPushPaymentInitiationStatus.Aborted: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushPaymentInitiations.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPushDebitTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentInitiations]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; + break; + case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: + newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh; + break; + case PeerPushPaymentInitiationStatus.SuspendedReady: + newStatus = PeerPushPaymentInitiationStatus.PendingReady; + break; + case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: + newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse; + break; + case PeerPushPaymentInitiationStatus.PendingCreatePurse: + case PeerPushPaymentInitiationStatus.AbortingRefresh: + case PeerPushPaymentInitiationStatus.AbortingDeletePurse: + case PeerPushPaymentInitiationStatus.PendingReady: + case PeerPushPaymentInitiationStatus.Done: + case PeerPushPaymentInitiationStatus.Aborted: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushPaymentInitiations.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export function computePeerPushCreditTransactionState( + pushCreditRecord: PeerPushPaymentIncomingRecord, +): TransactionState { + switch (pushCreditRecord.status) { + case PeerPushPaymentIncomingStatus.Proposed: + return { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }; + case PeerPushPaymentIncomingStatus.Accepted: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Merge, + }; + case PeerPushPaymentIncomingStatus.Done: + return { + major: TransactionMajorState.Done, + }; + case PeerPushPaymentIncomingStatus.MergeKycRequired: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }; + case PeerPushPaymentIncomingStatus.Withdrawing: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Withdraw, + }; + } +} + +export function computePeerPullCreditTransactionState( + pullCreditRecord: PeerPullPaymentInitiationRecord, +): TransactionState { + switch (pullCreditRecord.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.CreatePurse, + }; + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.MergeKycRequired, + }; + case PeerPullPaymentInitiationStatus.PendingReady: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }; + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + return { + major: TransactionMajorState.Done, + }; + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Withdraw, + }; + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.CreatePurse, + }; + case PeerPullPaymentInitiationStatus.SuspendedReady: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.Ready, + }; + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Withdraw, + }; + } +} + +export function computePeerPullDebitTransactionState( + pullDebitRecord: PeerPullPaymentIncomingRecord, +): TransactionState { + switch (pullDebitRecord.status) { + case PeerPullPaymentIncomingStatus.Proposed: + return { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }; + case PeerPullPaymentIncomingStatus.Accepted: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Deposit, + }; + case PeerPullPaymentIncomingStatus.Paid: + return { + major: TransactionMajorState.Done, + }; + } +} |