diff options
author | Florian Dold <florian@dold.me> | 2023-06-05 11:45:16 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-06-05 11:45:16 +0200 |
commit | fda5a0ed87a6473a6b34bd1ac07d5f1d45dfbc19 (patch) | |
tree | c8b7b09ca441d2a01e340dd3f569e075d3ef278e /packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts | |
parent | f3d4ff4e3a44141ad387ef68a9083b01bf1c818a (diff) | |
download | wallet-core-fda5a0ed87a6473a6b34bd1ac07d5f1d45dfbc19.tar.xz |
wallet-core: restructure p2p implv0.9.3-dev.14
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts | 910 |
1 files changed, 910 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts new file mode 100644 index 000000000..b9c9728a1 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -0,0 +1,910 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +import { + AbsoluteTime, + Amounts, + CancellationToken, + CheckPeerPullCreditRequest, + CheckPeerPullCreditResponse, + ContractTermsUtil, + ExchangeReservePurseRequest, + HttpStatusCode, + InitiatePeerPullCreditRequest, + InitiatePeerPullCreditResponse, + Logger, + TalerPreciseTimestamp, + TransactionAction, + TransactionMajorState, + TransactionMinorState, + TransactionState, + TransactionType, + WalletAccountMergeFlags, + codecForAny, + codecForWalletKycUuid, + constructPayPullUri, + encodeCrock, + getRandomBytes, + j2s, +} from "@gnu-taler/taler-util"; +import { + readSuccessResponseJsonOrErrorCode, + readSuccessResponseJsonOrThrow, + throwUnexpectedRequestError, +} from "@gnu-taler/taler-util/http"; +import { + PeerPullPaymentInitiationRecord, + PeerPullPaymentInitiationStatus, + WithdrawalGroupStatus, + WithdrawalRecordType, + updateExchangeFromUrl, +} from "../index.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; +import { PendingTaskType } from "../pending-types.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; +import { checkDbInvariant } from "../util/invariants.js"; +import { + OperationAttemptResult, + OperationAttemptResultType, + constructTaskIdentifier, +} from "../util/retries.js"; +import { + LongpollResult, + resetOperationTimeout, + runLongpollAsync, + runOperationWithErrorReporting, +} from "./common.js"; +import { + codecForExchangePurseStatus, + getMergeReserveInfo, + talerPaytoFromExchangeReserve, +} from "./pay-peer-common.js"; +import { + constructTransactionIdentifier, + notifyTransition, + stopLongpolling, +} from "./transactions.js"; +import { + checkWithdrawalKycStatus, + getExchangeWithdrawalInfo, + internalCreateWithdrawalGroup, + processWithdrawalGroup, +} from "./withdraw.js"; + +const logger = new Logger("pay-peer-pull-credit.ts"); + +export async function queryPurseForPeerPullCredit( + ws: InternalWalletState, + pullIni: PeerPullPaymentInitiationRecord, + cancellationToken: CancellationToken, +): Promise<LongpollResult> { + const purseDepositUrl = new URL( + `purses/${pullIni.pursePub}/deposit`, + pullIni.exchangeBaseUrl, + ); + purseDepositUrl.searchParams.set("timeout_ms", "30000"); + logger.info(`querying purse status via ${purseDepositUrl.href}`); + const resp = await ws.http.get(purseDepositUrl.href, { + timeout: { d_ms: 60000 }, + cancellationToken, + }); + + logger.info(`purse status code: HTTP ${resp.status}`); + + const result = await readSuccessResponseJsonOrErrorCode( + resp, + codecForExchangePurseStatus(), + ); + + if (result.isError) { + logger.info(`got purse status error, EC=${result.talerErrorResponse.code}`); + if (resp.status === 404) { + return { ready: false }; + } else { + throwUnexpectedRequestError(resp, result.talerErrorResponse); + } + } + + if (!result.response.deposit_timestamp) { + logger.info("purse not ready yet (no deposit)"); + return { ready: false }; + } + + const reserve = await ws.db + .mktx((x) => [x.reserves]) + .runReadOnly(async (tx) => { + return await tx.reserves.get(pullIni.mergeReserveRowId); + }); + + if (!reserve) { + throw Error("reserve for peer pull credit not found in wallet DB"); + } + + await internalCreateWithdrawalGroup(ws, { + amount: Amounts.parseOrThrow(pullIni.amount), + wgInfo: { + withdrawalType: WithdrawalRecordType.PeerPullCredit, + contractTerms: pullIni.contractTerms, + contractPriv: pullIni.contractPriv, + }, + forcedWithdrawalGroupId: pullIni.withdrawalGroupId, + exchangeBaseUrl: pullIni.exchangeBaseUrl, + reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, + reserveKeyPair: { + priv: reserve.reservePriv, + pub: reserve.reservePub, + }, + }); + + await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const finPi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub); + if (!finPi) { + logger.warn("peerPullPaymentInitiation not found anymore"); + return; + } + if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) { + finPi.status = PeerPullPaymentInitiationStatus.DonePurseDeposited; + } + await tx.peerPullPaymentInitiations.put(finPi); + }); + return { + ready: true, + }; +} + +export async function processPeerPullCredit( + ws: InternalWalletState, + pursePub: string, +): Promise<OperationAttemptResult> { + const pullIni = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadOnly(async (tx) => { + return tx.peerPullPaymentInitiations.get(pursePub); + }); + if (!pullIni) { + throw Error("peer pull payment initiation not found in database"); + } + + const retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + + // We're already running! + if (ws.activeLongpoll[retryTag]) { + logger.info("peer-pull-credit already in long-polling, returning!"); + return { + type: OperationAttemptResultType.Longpoll, + }; + } + + logger.trace(`processing ${retryTag}, status=${pullIni.status}`); + + switch (pullIni.status) { + case PeerPullPaymentInitiationStatus.DonePurseDeposited: { + // We implement this case so that the "retry" action on a peer-pull-credit transaction + // also retries the withdrawal task. + + logger.warn( + "peer pull payment initiation is already finished, retrying withdrawal", + ); + + const withdrawalGroupId = pullIni.withdrawalGroupId; + + if (withdrawalGroupId) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.Withdraw, + withdrawalGroupId, + }); + stopLongpolling(ws, taskId); + await resetOperationTimeout(ws, taskId); + await runOperationWithErrorReporting(ws, taskId, () => + processWithdrawalGroup(ws, withdrawalGroupId), + ); + } + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; + } + case PeerPullPaymentInitiationStatus.PendingReady: + runLongpollAsync(ws, retryTag, async (cancellationToken) => + queryPurseForPeerPullCredit(ws, pullIni, cancellationToken), + ); + logger.trace( + "returning early from processPeerPullCredit for long-polling in background", + ); + return { + type: OperationAttemptResultType.Longpoll, + }; + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub: pullIni.pursePub, + }); + if (pullIni.kycInfo) { + await checkWithdrawalKycStatus( + ws, + pullIni.exchangeBaseUrl, + transactionId, + pullIni.kycInfo, + "individual", + ); + } + break; + } + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + break; + default: + throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`); + } + + const mergeReserve = await ws.db + .mktx((x) => [x.reserves]) + .runReadOnly(async (tx) => { + return tx.reserves.get(pullIni.mergeReserveRowId); + }); + + if (!mergeReserve) { + throw Error("merge reserve for peer pull payment not found in database"); + } + + const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount)); + + const reservePayto = talerPaytoFromExchangeReserve( + pullIni.exchangeBaseUrl, + mergeReserve.reservePub, + ); + + const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ + contractPriv: pullIni.contractPriv, + contractPub: pullIni.contractPub, + contractTerms: pullIni.contractTerms, + pursePriv: pullIni.pursePriv, + pursePub: pullIni.pursePub, + }); + + const purseExpiration = pullIni.contractTerms.purse_expiration; + const sigRes = await ws.cryptoApi.signReservePurseCreate({ + contractTermsHash: pullIni.contractTermsHash, + flags: WalletAccountMergeFlags.CreateWithPurseFee, + mergePriv: pullIni.mergePriv, + mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), + purseAmount: pullIni.contractTerms.amount, + purseExpiration: purseExpiration, + purseFee: purseFee, + pursePriv: pullIni.pursePriv, + pursePub: pullIni.pursePub, + reservePayto, + reservePriv: mergeReserve.reservePriv, + }); + + const reservePurseReqBody: ExchangeReservePurseRequest = { + merge_sig: sigRes.mergeSig, + merge_timestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), + h_contract_terms: pullIni.contractTermsHash, + merge_pub: pullIni.mergePub, + min_age: 0, + purse_expiration: purseExpiration, + purse_fee: purseFee, + purse_pub: pullIni.pursePub, + purse_sig: sigRes.purseSig, + purse_value: pullIni.contractTerms.amount, + reserve_sig: sigRes.accountSig, + econtract: econtractResp.econtract, + }; + + logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`); + + const reservePurseMergeUrl = new URL( + `reserves/${mergeReserve.reservePub}/purse`, + pullIni.exchangeBaseUrl, + ); + + const httpResp = await ws.http.postJson( + reservePurseMergeUrl.href, + reservePurseReqBody, + ); + + if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { + const respJson = await httpResp.json(); + const kycPending = codecForWalletKycUuid().decode(respJson); + logger.info(`kyc uuid response: ${j2s(kycPending)}`); + + await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const peerIni = await tx.peerPullPaymentInitiations.get(pursePub); + if (!peerIni) { + return; + } + peerIni.kycInfo = { + paytoHash: kycPending.h_payto, + requirementRow: kycPending.requirement_row, + }; + peerIni.status = + PeerPullPaymentInitiationStatus.PendingMergeKycRequired; + await tx.peerPullPaymentInitiations.put(peerIni); + }); + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } + + const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); + + logger.info(`reserve merge response: ${j2s(resp)}`); + + await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pi2) { + return; + } + pi2.status = PeerPullPaymentInitiationStatus.PendingReady; + await tx.peerPullPaymentInitiations.put(pi2); + }); + + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; +} + +/** + * Check fees and available exchanges for a peer push payment initiation. + */ +export async function checkPeerPullPaymentInitiation( + ws: InternalWalletState, + req: CheckPeerPullCreditRequest, +): Promise<CheckPeerPullCreditResponse> { + // FIXME: We don't support exchanges with purse fees yet. + // Select an exchange where we have money in the specified currency + // FIXME: How do we handle regional currency scopes here? Is it an additional input? + + logger.trace("checking peer-pull-credit fees"); + + const currency = Amounts.currencyOf(req.amount); + let exchangeUrl; + if (req.exchangeBaseUrl) { + exchangeUrl = req.exchangeBaseUrl; + } else { + exchangeUrl = await getPreferredExchangeForCurrency(ws, currency); + } + + if (!exchangeUrl) { + throw Error("no exchange found for initiating a peer pull payment"); + } + + logger.trace(`found ${exchangeUrl} as preferred exchange`); + + const wi = await getExchangeWithdrawalInfo( + ws, + exchangeUrl, + Amounts.parseOrThrow(req.amount), + undefined, + ); + + logger.trace(`got withdrawal info`); + + return { + exchangeBaseUrl: exchangeUrl, + amountEffective: wi.withdrawalAmountEffective, + amountRaw: req.amount, + }; +} + +/** + * Find a preferred exchange based on when we withdrew last from this exchange. + */ +async function getPreferredExchangeForCurrency( + ws: InternalWalletState, + currency: string, +): Promise<string | undefined> { + // Find an exchange with the matching currency. + // Prefer exchanges with the most recent withdrawal. + const url = await ws.db + .mktx((x) => [x.exchanges]) + .runReadOnly(async (tx) => { + const exchanges = await tx.exchanges.iter().toArray(); + let candidate = undefined; + for (const e of exchanges) { + if (e.detailsPointer?.currency !== currency) { + continue; + } + if (!candidate) { + candidate = e; + continue; + } + if (candidate.lastWithdrawal && !e.lastWithdrawal) { + continue; + } + if (candidate.lastWithdrawal && e.lastWithdrawal) { + if ( + AbsoluteTime.cmp( + AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal), + AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal), + ) > 0 + ) { + candidate = e; + } + } + } + if (candidate) { + return candidate.baseUrl; + } + return undefined; + }); + return url; +} + +/** + * Initiate a peer pull payment. + */ +export async function initiatePeerPullPayment( + ws: InternalWalletState, + req: InitiatePeerPullCreditRequest, +): Promise<InitiatePeerPullCreditResponse> { + const currency = Amounts.currencyOf(req.partialContractTerms.amount); + let maybeExchangeBaseUrl: string | undefined; + if (req.exchangeBaseUrl) { + maybeExchangeBaseUrl = req.exchangeBaseUrl; + } else { + maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency); + } + + if (!maybeExchangeBaseUrl) { + throw Error("no exchange found for initiating a peer pull payment"); + } + + const exchangeBaseUrl = maybeExchangeBaseUrl; + + await updateExchangeFromUrl(ws, exchangeBaseUrl); + + const mergeReserveInfo = await getMergeReserveInfo(ws, { + exchangeBaseUrl: exchangeBaseUrl, + }); + + const mergeTimestamp = TalerPreciseTimestamp.now(); + + const pursePair = await ws.cryptoApi.createEddsaKeypair({}); + const mergePair = await ws.cryptoApi.createEddsaKeypair({}); + + const contractTerms = req.partialContractTerms; + + const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + + const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({}); + + const withdrawalGroupId = encodeCrock(getRandomBytes(32)); + + const mergeReserveRowId = mergeReserveInfo.rowId; + checkDbInvariant(!!mergeReserveRowId); + + const wi = await getExchangeWithdrawalInfo( + ws, + exchangeBaseUrl, + Amounts.parseOrThrow(req.partialContractTerms.amount), + undefined, + ); + + await ws.db + .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms]) + .runReadWrite(async (tx) => { + await tx.peerPullPaymentInitiations.put({ + amount: req.partialContractTerms.amount, + contractTermsHash: hContractTerms, + exchangeBaseUrl: exchangeBaseUrl, + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + mergePriv: mergePair.priv, + mergePub: mergePair.pub, + status: PeerPullPaymentInitiationStatus.PendingCreatePurse, + contractTerms: contractTerms, + mergeTimestamp, + mergeReserveRowId: mergeReserveRowId, + contractPriv: contractKeyPair.priv, + contractPub: contractKeyPair.pub, + withdrawalGroupId, + estimatedAmountEffective: wi.withdrawalAmountEffective, + }); + await tx.contractTerms.put({ + contractTermsRaw: contractTerms, + h: hContractTerms, + }); + }); + + // FIXME: Should we somehow signal to the client + // whether purse creation has failed, or does the client/ + // check this asynchronously from the transaction status? + + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub: pursePair.pub, + }); + + await runOperationWithErrorReporting(ws, taskId, async () => { + return processPeerPullCredit(ws, pursePair.pub); + }); + + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub: pursePair.pub, + }); + + return { + talerUri: constructPayPullUri({ + exchangeBaseUrl: exchangeBaseUrl, + contractPriv: contractKeyPair.priv, + }), + transactionId, + }; +} + +export async function suspendPeerPullCreditTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse; + break; + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired; + break; + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing; + break; + case PeerPullPaymentInitiationStatus.PendingReady: + newStatus = PeerPullPaymentInitiationStatus.SuspendedReady; + break; + case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + newStatus = + PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse; + break; + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + case PeerPullPaymentInitiationStatus.SuspendedReady: + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + case PeerPullPaymentInitiationStatus.Aborted: + case PeerPullPaymentInitiationStatus.Failed: + case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullPaymentInitiations.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function abortPeerPullCreditTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; + break; + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + throw Error("can't abort anymore"); + case PeerPullPaymentInitiationStatus.PendingReady: + newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; + break; + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + case PeerPullPaymentInitiationStatus.SuspendedReady: + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + case PeerPullPaymentInitiationStatus.Aborted: + case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + case PeerPullPaymentInitiationStatus.Failed: + case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullPaymentInitiations.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failPeerPullCreditTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentInitiationStatus.PendingReady: + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + case PeerPullPaymentInitiationStatus.SuspendedReady: + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + case PeerPullPaymentInitiationStatus.Aborted: + case PeerPullPaymentInitiationStatus.Failed: + break; + case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPullPaymentInitiationStatus.Failed; + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullPaymentInitiations.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPullCreditTransaction( + ws: InternalWalletState, + pursePub: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentInitiationStatus.PendingReady: + case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + case PeerPullPaymentInitiationStatus.Failed: + case PeerPullPaymentInitiationStatus.Aborted: + break; + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse; + break; + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; + break; + case PeerPullPaymentInitiationStatus.SuspendedReady: + newStatus = PeerPullPaymentInitiationStatus.PendingReady; + break; + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing; + break; + case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullPaymentInitiations.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + +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, + }; + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.MergeKycRequired, + }; + case PeerPullPaymentInitiationStatus.Aborted: + return { + major: TransactionMajorState.Aborted, + }; + case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + return { + major: TransactionMajorState.Aborting, + minor: TransactionMinorState.DeletePurse, + }; + case PeerPullPaymentInitiationStatus.Failed: + return { + major: TransactionMajorState.Failed, + }; + case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + return { + major: TransactionMajorState.Aborting, + minor: TransactionMinorState.DeletePurse, + }; + } +} + +export function computePeerPullCreditTransactionActions( + pullCreditRecord: PeerPullPaymentInitiationRecord, +): TransactionAction[] { + switch (pullCreditRecord.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + return [TransactionAction.Abort, TransactionAction.Suspend]; + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + return [TransactionAction.Abort, TransactionAction.Suspend]; + case PeerPullPaymentInitiationStatus.PendingReady: + return [TransactionAction.Abort, TransactionAction.Suspend]; + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + return [TransactionAction.Delete]; + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + return [TransactionAction.Abort, TransactionAction.Suspend]; + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + return [TransactionAction.Resume, TransactionAction.Abort]; + case PeerPullPaymentInitiationStatus.SuspendedReady: + return [TransactionAction.Abort, TransactionAction.Resume]; + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + return [TransactionAction.Resume, TransactionAction.Fail]; + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + return [TransactionAction.Resume, TransactionAction.Fail]; + case PeerPullPaymentInitiationStatus.Aborted: + return [TransactionAction.Delete]; + case PeerPullPaymentInitiationStatus.AbortingDeletePurse: + return [TransactionAction.Suspend, TransactionAction.Fail]; + case PeerPullPaymentInitiationStatus.Failed: + return [TransactionAction.Delete]; + case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: + return [TransactionAction.Resume, TransactionAction.Fail]; + } +} |