/* 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 { 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, TalerError, TalerErrorCode, WalletKycUuid, makeErrorDetail, } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { InternalWalletState, KycPendingInfo, KycUserType, 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 { 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"; import { runLongpollAsync } from "./common.js"; const logger = new Logger("pay-peer-push-credit.ts"); export async function preparePeerPushCredit( ws: InternalWalletState, req: PreparePeerPushCredit, ): Promise { 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, }), }; } async function longpollKycStatus( ws: InternalWalletState, peerPushPaymentIncomingId: string, exchangeUrl: string, kycInfo: KycPendingInfo, userType: KycUserType, ): Promise { const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, peerPushPaymentIncomingId, }); const retryTag = constructTaskIdentifier({ tag: PendingTaskType.PeerPushCredit, peerPushPaymentIncomingId, }); runLongpollAsync(ws, retryTag, async (ct) => { const url = new URL( `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, exchangeUrl, ); url.searchParams.set("timeout_ms", "10000"); logger.info(`kyc url ${url.href}`); const kycStatusRes = await ws.http.fetch(url.href, { method: "GET", cancellationToken: ct, }); if ( kycStatusRes.status === HttpStatusCode.Ok || //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge // remove after the exchange is fixed or clarified kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await ws.db .mktx((x) => [x.peerPushPaymentIncoming]) .runReadWrite(async (tx) => { const peerInc = await tx.peerPushPaymentIncoming.get( peerPushPaymentIncomingId, ); if (!peerInc) { return; } if ( peerInc.status !== PeerPushPaymentIncomingStatus.PendingMergeKycRequired ) { return; } const oldTxState = computePeerPushCreditTransactionState(peerInc); peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; const newTxState = computePeerPushCreditTransactionState(peerInc); await tx.peerPushPaymentIncoming.put(peerInc); return { oldTxState, newTxState }; }); notifyTransition(ws, transactionId, transitionInfo); return { ready: true }; } else if (kycStatusRes.status === HttpStatusCode.Accepted) { // FIXME: Do we have to update the URL here? return { ready: false }; } else { throw Error( `unexpected response from kyc-check (${kycStatusRes.status})`, ); } }); return { type: OperationAttemptResultType.Longpoll, }; } async function processPeerPushCreditKycRequired( ws: InternalWalletState, peerInc: PeerPushPaymentIncomingRecord, kycPending: WalletKycUuid, ): Promise { const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, }); const { peerPushPaymentIncomingId } = peerInc; const userType = "individual"; const url = new URL( `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`, peerInc.exchangeBaseUrl, ); logger.info(`kyc url ${url.href}`); const kycStatusRes = await ws.http.fetch(url.href, { method: "GET", }); if ( kycStatusRes.status === HttpStatusCode.Ok || //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge // remove after the exchange is fixed or clarified kycStatusRes.status === HttpStatusCode.NoContent ) { logger.warn("kyc requested, but already fulfilled"); return { type: OperationAttemptResultType.Finished, result: undefined, }; } else if (kycStatusRes.status === HttpStatusCode.Accepted) { const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); const { transitionInfo, result } = await ws.db .mktx((x) => [x.peerPushPaymentIncoming]) .runReadWrite(async (tx) => { const peerInc = await tx.peerPushPaymentIncoming.get( peerPushPaymentIncomingId, ); if (!peerInc) { return { transitionInfo: undefined, result: OperationAttemptResult.finishedEmpty(), }; } const oldTxState = computePeerPushCreditTransactionState(peerInc); peerInc.kycInfo = { paytoHash: kycPending.h_payto, requirementRow: kycPending.requirement_row, }; peerInc.kycUrl = kycStatus.kyc_url; peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; const newTxState = computePeerPushCreditTransactionState(peerInc); await tx.peerPushPaymentIncoming.put(peerInc); // We'll remove this eventually! New clients should rely on the // kycUrl field of the transaction, not the error code. const res: OperationAttemptResult = { type: OperationAttemptResultType.Error, errorDetail: makeErrorDetail( TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, { kycUrl: kycStatus.kyc_url, }, ), }; return { transitionInfo: { oldTxState, newTxState }, result: res, }; }); notifyTransition(ws, transactionId, transitionInfo); return result; } else { throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); } } export async function processPeerPushCredit( ws: InternalWalletState, peerPushPaymentIncomingId: string, ): Promise { 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 ) { if (!peerInc.kycInfo) { throw Error("invalid state, kycInfo required"); } return await longpollKycStatus( ws, peerPushPaymentIncomingId, peerInc.exchangeBaseUrl, 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.fetch(mergePurseUrl.href, { method: "POST", body: mergeReq, }); if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { const respJson = await mergeHttpResp.json(); const kycPending = codecForWalletKycUuid().decode(respJson); logger.info(`kyc uuid response: ${j2s(kycPending)}`); processPeerPushCreditKycRequired(ws, peerInc, kycPending); } 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 { 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 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); } }