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-push-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-push-credit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts | 770 |
1 files changed, 770 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts new file mode 100644 index 000000000..69e0f3c27 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -0,0 +1,770 @@ +/* + 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 { + PreparePeerPushCredit, + PreparePeerPushCreditResponse, + parsePayPushUri, + codecForPeerContractTerms, + TransactionType, + encodeCrock, + eddsaGetPublic, + decodeCrock, + codecForExchangeGetContractResponse, + getRandomBytes, + ContractTermsUtil, + Amounts, + TalerPreciseTimestamp, + AcceptPeerPushPaymentResponse, + ConfirmPeerPushCreditRequest, + ExchangePurseMergeRequest, + HttpStatusCode, + PeerContractTerms, + TalerProtocolTimestamp, + WalletAccountMergeFlags, + codecForAny, + codecForWalletKycUuid, + j2s, + Logger, + ExchangePurseDeposits, + TransactionAction, + TransactionMajorState, + TransactionMinorState, + TransactionState, +} from "@gnu-taler/taler-util"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + InternalWalletState, + PeerPullDebitRecordStatus, + PeerPushPaymentIncomingRecord, + PeerPushPaymentIncomingStatus, + PendingTaskType, + WithdrawalGroupStatus, + WithdrawalRecordType, +} from "../index.js"; +import { updateExchangeFromUrl } from "./exchanges.js"; +import { + codecForExchangePurseStatus, + getMergeReserveInfo, + queryCoinInfosForSelection, + talerPaytoFromExchangeReserve, +} from "./pay-peer-common.js"; +import { constructTransactionIdentifier, notifyTransition, stopLongpolling } from "./transactions.js"; +import { + checkWithdrawalKycStatus, + getExchangeWithdrawalInfo, + internalCreateWithdrawalGroup, +} from "./withdraw.js"; +import { checkDbInvariant } from "../util/invariants.js"; +import { + OperationAttemptResult, + OperationAttemptResultType, + constructTaskIdentifier, +} from "../util/retries.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; + +const logger = new Logger("pay-peer-push-credit.ts"); + +export async function preparePeerPushCredit( + ws: InternalWalletState, + req: PreparePeerPushCredit, +): Promise<PreparePeerPushCreditResponse> { + const uri = parsePayPushUri(req.talerUri); + + if (!uri) { + throw Error("got invalid taler://pay-push URI"); + } + + const existing = await ws.db + .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .runReadOnly(async (tx) => { + const existingPushInc = + await tx.peerPushPaymentIncoming.indexes.byExchangeAndContractPriv.get([ + uri.exchangeBaseUrl, + uri.contractPriv, + ]); + if (!existingPushInc) { + return; + } + const existingContractTermsRec = await tx.contractTerms.get( + existingPushInc.contractTermsHash, + ); + if (!existingContractTermsRec) { + throw Error( + "contract terms for peer push payment credit not found in database", + ); + } + const existingContractTerms = codecForPeerContractTerms().decode( + existingContractTermsRec.contractTermsRaw, + ); + return { existingPushInc, existingContractTerms }; + }); + + if (existing) { + return { + amount: existing.existingContractTerms.amount, + amountEffective: existing.existingPushInc.estimatedAmountEffective, + amountRaw: existing.existingContractTerms.amount, + contractTerms: existing.existingContractTerms, + peerPushPaymentIncomingId: + existing.existingPushInc.peerPushPaymentIncomingId, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId: + existing.existingPushInc.peerPushPaymentIncomingId, + }), + }; + } + + const exchangeBaseUrl = uri.exchangeBaseUrl; + + await updateExchangeFromUrl(ws, exchangeBaseUrl); + + const contractPriv = uri.contractPriv; + const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); + + const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); + + const contractHttpResp = await ws.http.get(getContractUrl.href); + + const contractResp = await readSuccessResponseJsonOrThrow( + contractHttpResp, + codecForExchangeGetContractResponse(), + ); + + const pursePub = contractResp.purse_pub; + + const dec = await ws.cryptoApi.decryptContractForMerge({ + ciphertext: contractResp.econtract, + contractPriv: contractPriv, + pursePub: pursePub, + }); + + const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl); + + const purseHttpResp = await ws.http.get(getPurseUrl.href); + + const purseStatus = await readSuccessResponseJsonOrThrow( + purseHttpResp, + codecForExchangePurseStatus(), + ); + + const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32)); + + const contractTermsHash = ContractTermsUtil.hashContractTerms( + dec.contractTerms, + ); + + const withdrawalGroupId = encodeCrock(getRandomBytes(32)); + + const wi = await getExchangeWithdrawalInfo( + ws, + exchangeBaseUrl, + Amounts.parseOrThrow(purseStatus.balance), + undefined, + ); + + await ws.db + .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + await tx.peerPushPaymentIncoming.add({ + peerPushPaymentIncomingId, + contractPriv: contractPriv, + exchangeBaseUrl: exchangeBaseUrl, + mergePriv: dec.mergePriv, + pursePub: pursePub, + timestamp: TalerPreciseTimestamp.now(), + contractTermsHash, + status: PeerPushPaymentIncomingStatus.DialogProposed, + withdrawalGroupId, + currency: Amounts.currencyOf(purseStatus.balance), + estimatedAmountEffective: Amounts.stringify( + wi.withdrawalAmountEffective, + ), + }); + + await tx.contractTerms.put({ + h: contractTermsHash, + contractTermsRaw: dec.contractTerms, + }); + }); + + return { + amount: purseStatus.balance, + amountEffective: wi.withdrawalAmountEffective, + amountRaw: purseStatus.balance, + contractTerms: dec.contractTerms, + peerPushPaymentIncomingId, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }), + }; +} + +export async function processPeerPushCredit( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +): Promise<OperationAttemptResult> { + let peerInc: PeerPushPaymentIncomingRecord | undefined; + let contractTerms: PeerContractTerms | undefined; + await ws.db + .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId); + if (!peerInc) { + return; + } + const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash); + if (ctRec) { + contractTerms = ctRec.contractTermsRaw; + } + await tx.peerPushPaymentIncoming.put(peerInc); + }); + + if (!peerInc) { + throw Error( + `can't accept unknown incoming p2p push payment (${peerPushPaymentIncomingId})`, + ); + } + + checkDbInvariant(!!contractTerms); + + const amount = Amounts.parseOrThrow(contractTerms.amount); + + if ( + peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired && + peerInc.kycInfo + ) { + const txId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, + }); + await checkWithdrawalKycStatus( + ws, + peerInc.exchangeBaseUrl, + txId, + peerInc.kycInfo, + "individual", + ); + } + + const mergeReserveInfo = await getMergeReserveInfo(ws, { + exchangeBaseUrl: peerInc.exchangeBaseUrl, + }); + + const mergeTimestamp = TalerProtocolTimestamp.now(); + + const reservePayto = talerPaytoFromExchangeReserve( + peerInc.exchangeBaseUrl, + mergeReserveInfo.reservePub, + ); + + const sigRes = await ws.cryptoApi.signPurseMerge({ + contractTermsHash: ContractTermsUtil.hashContractTerms(contractTerms), + flags: WalletAccountMergeFlags.MergeFullyPaidPurse, + mergePriv: peerInc.mergePriv, + mergeTimestamp: mergeTimestamp, + purseAmount: Amounts.stringify(amount), + purseExpiration: contractTerms.purse_expiration, + purseFee: Amounts.stringify(Amounts.zeroOfCurrency(amount.currency)), + pursePub: peerInc.pursePub, + reservePayto, + reservePriv: mergeReserveInfo.reservePriv, + }); + + const mergePurseUrl = new URL( + `purses/${peerInc.pursePub}/merge`, + peerInc.exchangeBaseUrl, + ); + + const mergeReq: ExchangePurseMergeRequest = { + payto_uri: reservePayto, + merge_timestamp: mergeTimestamp, + merge_sig: sigRes.mergeSig, + reserve_sig: sigRes.accountSig, + }; + + const mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq); + + if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { + const respJson = await mergeHttpResp.json(); + const kycPending = codecForWalletKycUuid().decode(respJson); + logger.info(`kyc uuid response: ${j2s(kycPending)}`); + + await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const peerInc = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!peerInc) { + return; + } + peerInc.kycInfo = { + paytoHash: kycPending.h_payto, + requirementRow: kycPending.requirement_row, + }; + peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; + await tx.peerPushPaymentIncoming.put(peerInc); + }); + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } + + logger.trace(`merge request: ${j2s(mergeReq)}`); + const res = await readSuccessResponseJsonOrThrow( + mergeHttpResp, + codecForAny(), + ); + logger.trace(`merge response: ${j2s(res)}`); + + await internalCreateWithdrawalGroup(ws, { + amount, + wgInfo: { + withdrawalType: WithdrawalRecordType.PeerPushCredit, + contractTerms, + }, + forcedWithdrawalGroupId: peerInc.withdrawalGroupId, + exchangeBaseUrl: peerInc.exchangeBaseUrl, + reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, + reserveKeyPair: { + priv: mergeReserveInfo.reservePriv, + pub: mergeReserveInfo.reservePub, + }, + }); + + await ws.db + .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const peerInc = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!peerInc) { + return; + } + if ( + peerInc.status === PeerPushPaymentIncomingStatus.PendingMerge || + peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired + ) { + peerInc.status = PeerPushPaymentIncomingStatus.Done; + } + await tx.peerPushPaymentIncoming.put(peerInc); + }); + + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; +} + +export async function confirmPeerPushCredit( + ws: InternalWalletState, + req: ConfirmPeerPushCreditRequest, +): Promise<AcceptPeerPushPaymentResponse> { + let peerInc: PeerPushPaymentIncomingRecord | undefined; + + await ws.db + .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + peerInc = await tx.peerPushPaymentIncoming.get( + req.peerPushPaymentIncomingId, + ); + if (!peerInc) { + return; + } + if (peerInc.status === PeerPushPaymentIncomingStatus.DialogProposed) { + peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; + } + await tx.peerPushPaymentIncoming.put(peerInc); + }); + + if (!peerInc) { + throw Error( + `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`, + ); + } + + ws.workAvailable.trigger(); + + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId: req.peerPushPaymentIncomingId, + }); + + return { + transactionId, + }; +} + + +export async function processPeerPullDebit( + ws: InternalWalletState, + peerPullPaymentIncomingId: string, +): Promise<OperationAttemptResult> { + const peerPullInc = await ws.db + .mktx((x) => [x.peerPullPaymentIncoming]) + .runReadOnly(async (tx) => { + return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId); + }); + if (!peerPullInc) { + throw Error("peer pull debit not found"); + } + if (peerPullInc.status === PeerPullDebitRecordStatus.PendingDeposit) { + 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 httpResp = await ws.http.postJson( + purseDepositUrl.href, + depositPayload, + ); + const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); + logger.trace(`purse deposit response: ${j2s(resp)}`); + } + + 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) { + pi.status = PeerPullDebitRecordStatus.DonePaid; + } + await tx.peerPullPaymentIncoming.put(pi); + }); + + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; +} + + +export async function suspendPeerPushCreditTransaction( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const pushCreditRec = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + case PeerPushPaymentIncomingStatus.Done: + case PeerPushPaymentIncomingStatus.SuspendedMerge: + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + break; + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired; + break; + case PeerPushPaymentIncomingStatus.PendingMerge: + newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge; + break; + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + // FIXME: Suspend internal withdrawal transaction! + newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing; + break; + case PeerPushPaymentIncomingStatus.Aborted: + break; + case PeerPushPaymentIncomingStatus.Failed: + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushPaymentIncoming.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function abortPeerPushCreditTransaction( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const pushCreditRec = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + newStatus = PeerPushPaymentIncomingStatus.Aborted; + break; + case PeerPushPaymentIncomingStatus.Done: + break; + case PeerPushPaymentIncomingStatus.SuspendedMerge: + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + newStatus = PeerPushPaymentIncomingStatus.Aborted; + break; + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + newStatus = PeerPushPaymentIncomingStatus.Aborted; + break; + case PeerPushPaymentIncomingStatus.PendingMerge: + newStatus = PeerPushPaymentIncomingStatus.Aborted; + break; + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + newStatus = PeerPushPaymentIncomingStatus.Aborted; + break; + case PeerPushPaymentIncomingStatus.Aborted: + break; + case PeerPushPaymentIncomingStatus.Failed: + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushPaymentIncoming.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failPeerPushCreditTransaction( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +) { + // We don't have any "aborting" states! + throw Error("can't run cancel-aborting on peer-push-credit transaction"); +} + +export async function resumePeerPushCreditTransaction( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const pushCreditRec = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + case PeerPushPaymentIncomingStatus.Done: + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + case PeerPushPaymentIncomingStatus.PendingMerge: + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + case PeerPushPaymentIncomingStatus.SuspendedMerge: + newStatus = PeerPushPaymentIncomingStatus.PendingMerge; + break; + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; + break; + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + // FIXME: resume underlying "internal-withdrawal" transaction. + newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing; + break; + case PeerPushPaymentIncomingStatus.Aborted: + break; + case PeerPushPaymentIncomingStatus.Failed: + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushPaymentIncoming.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + +export function computePeerPushCreditTransactionState( + pushCreditRecord: PeerPushPaymentIncomingRecord, +): TransactionState { + switch (pushCreditRecord.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + return { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }; + case PeerPushPaymentIncomingStatus.PendingMerge: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Merge, + }; + case PeerPushPaymentIncomingStatus.Done: + return { + major: TransactionMajorState.Done, + }; + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }; + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Withdraw, + }; + case PeerPushPaymentIncomingStatus.SuspendedMerge: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.Merge, + }; + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.MergeKycRequired, + }; + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.Withdraw, + }; + case PeerPushPaymentIncomingStatus.Aborted: + return { + major: TransactionMajorState.Aborted, + }; + case PeerPushPaymentIncomingStatus.Failed: + return { + major: TransactionMajorState.Failed, + }; + default: + assertUnreachable(pushCreditRecord.status); + } +} + +export function computePeerPushCreditTransactionActions( + pushCreditRecord: PeerPushPaymentIncomingRecord, +): TransactionAction[] { + switch (pushCreditRecord.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + return []; + case PeerPushPaymentIncomingStatus.PendingMerge: + return [TransactionAction.Abort, TransactionAction.Suspend]; + case PeerPushPaymentIncomingStatus.Done: + return [TransactionAction.Delete]; + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + return [TransactionAction.Abort, TransactionAction.Suspend]; + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + return [TransactionAction.Suspend, TransactionAction.Fail]; + case PeerPushPaymentIncomingStatus.SuspendedMerge: + return [TransactionAction.Resume, TransactionAction.Abort]; + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + return [TransactionAction.Resume, TransactionAction.Abort]; + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + return [TransactionAction.Resume, TransactionAction.Fail]; + case PeerPushPaymentIncomingStatus.Aborted: + return [TransactionAction.Delete]; + case PeerPushPaymentIncomingStatus.Failed: + return [TransactionAction.Delete]; + default: + assertUnreachable(pushCreditRecord.status); + } +}
\ No newline at end of file |