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.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.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer.ts | 3226 |
1 files changed, 0 insertions, 3226 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts deleted file mode 100644 index 28fef6afc..000000000 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ /dev/null @@ -1,3226 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 GNUnet e.V. - - 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/> - */ - -/** - * Imports. - */ -import { - AbsoluteTime, - ConfirmPeerPullDebitRequest, - AcceptPeerPullPaymentResponse, - ConfirmPeerPushCreditRequest, - AcceptPeerPushPaymentResponse, - AgeCommitmentProof, - AmountJson, - Amounts, - AmountString, - buildCodecForObject, - PreparePeerPullDebitRequest, - PreparePeerPullDebitResponse, - PreparePeerPushCredit, - PreparePeerPushCreditResponse, - Codec, - codecForAmountString, - codecForAny, - codecForExchangeGetContractResponse, - codecForPeerContractTerms, - CoinStatus, - constructPayPullUri, - constructPayPushUri, - ContractTermsUtil, - decodeCrock, - eddsaGetPublic, - encodeCrock, - ExchangePurseDeposits, - ExchangePurseMergeRequest, - ExchangeReservePurseRequest, - getRandomBytes, - InitiatePeerPullCreditRequest, - InitiatePeerPullCreditResponse, - InitiatePeerPushDebitRequest, - InitiatePeerPushDebitResponse, - j2s, - Logger, - parsePayPullUri, - parsePayPushUri, - PayPeerInsufficientBalanceDetails, - PeerContractTerms, - CheckPeerPullCreditRequest, - CheckPeerPullCreditResponse, - CheckPeerPushDebitRequest, - CheckPeerPushDebitResponse, - RefreshReason, - strcmp, - TalerErrorCode, - TalerProtocolTimestamp, - TransactionType, - UnblindedSignature, - WalletAccountMergeFlags, - codecOptional, - codecForTimestamp, - CancellationToken, - NotificationType, - HttpStatusCode, - codecForWalletKycUuid, - TransactionState, - TransactionMajorState, - TransactionMinorState, - TalerPreciseTimestamp, - TransactionAction, -} from "@gnu-taler/taler-util"; -import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; -import { - DenominationRecord, - PeerPullPaymentIncomingRecord, - PeerPullDebitRecordStatus, - PeerPullPaymentInitiationRecord, - PeerPullPaymentInitiationStatus, - PeerPushPaymentCoinSelection, - PeerPushPaymentIncomingRecord, - PeerPushPaymentIncomingStatus, - PeerPushPaymentInitiationRecord, - PeerPushPaymentInitiationStatus, - ReserveRecord, - WithdrawalGroupStatus, - WithdrawalRecordType, -} from "../db.js"; -import { TalerError } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../internal-wallet-state.js"; -import { - LongpollResult, - resetOperationTimeout, - runLongpollAsync, - runOperationWithErrorReporting, - spendCoins, -} from "../operations/common.js"; -import { - readSuccessResponseJsonOrErrorCode, - readSuccessResponseJsonOrThrow, - throwUnexpectedRequestError, -} from "@gnu-taler/taler-util/http"; -import { checkDbInvariant } from "../util/invariants.js"; -import { - constructTaskIdentifier, - OperationAttemptResult, - OperationAttemptResultType, - TaskIdentifiers, -} from "../util/retries.js"; -import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; -import { updateExchangeFromUrl } from "./exchanges.js"; -import { getTotalRefreshCost } from "./refresh.js"; -import { - checkWithdrawalKycStatus, - getExchangeWithdrawalInfo, - internalCreateWithdrawalGroup, - processWithdrawalGroup, -} from "./withdraw.js"; -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"); - -interface SelectedPeerCoin { - coinPub: string; - coinPriv: string; - contribution: AmountString; - denomPubHash: string; - denomSig: UnblindedSignature; - ageCommitmentProof: AgeCommitmentProof | undefined; -} - -interface PeerCoinSelectionDetails { - exchangeBaseUrl: string; - - /** - * Info of Coins that were selected. - */ - coins: SelectedPeerCoin[]; - - /** - * How much of the deposit fees is the customer paying? - */ - depositFees: AmountJson; -} - -/** - * Information about a selected coin for peer to peer payments. - */ -interface CoinInfo { - /** - * Public key of the coin. - */ - coinPub: string; - - coinPriv: string; - - /** - * Deposit fee for the coin. - */ - feeDeposit: AmountJson; - - value: AmountJson; - - denomPubHash: string; - - denomSig: UnblindedSignature; - - maxAge: number; - - ageCommitmentProof?: AgeCommitmentProof; -} - -export type SelectPeerCoinsResult = - | { type: "success"; result: PeerCoinSelectionDetails } - | { - type: "failure"; - insufficientBalanceDetails: PayPeerInsufficientBalanceDetails; - }; - -export async function queryCoinInfosForSelection( - ws: InternalWalletState, - csel: PeerPushPaymentCoinSelection, -): Promise<SpendCoinDetails[]> { - let infos: SpendCoinDetails[] = []; - await ws.db - .mktx((x) => [x.coins, x.denominations]) - .runReadOnly(async (tx) => { - for (let i = 0; i < csel.coinPubs.length; i++) { - const coin = await tx.coins.get(csel.coinPubs[i]); - if (!coin) { - throw Error("coin not found anymore"); - } - const denom = await ws.getDenomInfo( - ws, - tx, - coin.exchangeBaseUrl, - coin.denomPubHash, - ); - if (!denom) { - throw Error("denom for coin not found anymore"); - } - infos.push({ - coinPriv: coin.coinPriv, - coinPub: coin.coinPub, - denomPubHash: coin.denomPubHash, - denomSig: coin.denomSig, - ageCommitmentProof: coin.ageCommitmentProof, - contribution: csel.contributions[i], - }); - } - }); - return infos; -} - -export async function selectPeerCoins( - ws: InternalWalletState, - instructedAmount: AmountJson, -): Promise<SelectPeerCoinsResult> { - if (Amounts.isZero(instructedAmount)) { - // Other parts of the code assume that we have at least - // one coin to spend. - throw new Error("amount of zero not allowed"); - } - return await ws.db - .mktx((x) => [ - x.exchanges, - x.contractTerms, - x.coins, - x.coinAvailability, - x.denominations, - x.refreshGroups, - x.peerPushPaymentInitiations, - ]) - .runReadWrite(async (tx) => { - const exchanges = await tx.exchanges.iter().toArray(); - const exchangeFeeGap: { [url: string]: AmountJson } = {}; - const currency = Amounts.currencyOf(instructedAmount); - for (const exch of exchanges) { - if (exch.detailsPointer?.currency !== currency) { - continue; - } - const coins = ( - await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl) - ).filter((x) => x.status === CoinStatus.Fresh); - const coinInfos: CoinInfo[] = []; - for (const coin of coins) { - const denom = await ws.getDenomInfo( - ws, - tx, - coin.exchangeBaseUrl, - coin.denomPubHash, - ); - if (!denom) { - throw Error("denom not found"); - } - coinInfos.push({ - coinPub: coin.coinPub, - feeDeposit: Amounts.parseOrThrow(denom.feeDeposit), - value: Amounts.parseOrThrow(denom.value), - denomPubHash: denom.denomPubHash, - coinPriv: coin.coinPriv, - denomSig: coin.denomSig, - maxAge: coin.maxAge, - ageCommitmentProof: coin.ageCommitmentProof, - }); - } - if (coinInfos.length === 0) { - continue; - } - coinInfos.sort( - (o1, o2) => - -Amounts.cmp(o1.value, o2.value) || - strcmp(o1.denomPubHash, o2.denomPubHash), - ); - let amountAcc = Amounts.zeroOfCurrency(currency); - let depositFeesAcc = Amounts.zeroOfCurrency(currency); - const resCoins: { - coinPub: string; - coinPriv: string; - contribution: AmountString; - denomPubHash: string; - denomSig: UnblindedSignature; - ageCommitmentProof: AgeCommitmentProof | undefined; - }[] = []; - let lastDepositFee = Amounts.zeroOfCurrency(currency); - for (const coin of coinInfos) { - if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { - break; - } - const gap = Amounts.add( - coin.feeDeposit, - Amounts.sub(instructedAmount, amountAcc).amount, - ).amount; - const contrib = Amounts.min(gap, coin.value); - amountAcc = Amounts.add( - amountAcc, - Amounts.sub(contrib, coin.feeDeposit).amount, - ).amount; - depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount; - resCoins.push({ - coinPriv: coin.coinPriv, - coinPub: coin.coinPub, - contribution: Amounts.stringify(contrib), - denomPubHash: coin.denomPubHash, - denomSig: coin.denomSig, - ageCommitmentProof: coin.ageCommitmentProof, - }); - lastDepositFee = coin.feeDeposit; - } - if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { - const res: PeerCoinSelectionDetails = { - exchangeBaseUrl: exch.baseUrl, - coins: resCoins, - depositFees: depositFeesAcc, - }; - return { type: "success", result: res }; - } - const diff = Amounts.sub(instructedAmount, amountAcc).amount; - exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount; - - continue; - } - // We were unable to select coins. - // Now we need to produce error details. - - const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, { - currency, - }); - - const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {}; - - let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency); - - for (const exch of exchanges) { - if (exch.detailsPointer?.currency !== currency) { - continue; - } - const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, { - currency, - restrictExchangeTo: exch.baseUrl, - }); - let gap = - exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency); - if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) { - // Show fee gap only if we should've been able to pay with the material amount - gap = Amounts.zeroOfCurrency(currency); - } - perExchange[exch.baseUrl] = { - balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable), - balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial), - feeGapEstimate: Amounts.stringify(gap), - }; - - maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap); - } - - const errDetails: PayPeerInsufficientBalanceDetails = { - amountRequested: Amounts.stringify(instructedAmount), - balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable), - balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial), - feeGapEstimate: Amounts.stringify(maxFeeGapEstimate), - perExchange, - }; - - return { type: "failure", insufficientBalanceDetails: errDetails }; - }); -} - -export async function getTotalPeerPaymentCost( - ws: InternalWalletState, - pcs: SelectedPeerCoin[], -): Promise<AmountJson> { - return ws.db - .mktx((x) => [x.coins, x.denominations]) - .runReadOnly(async (tx) => { - const costs: AmountJson[] = []; - for (let i = 0; i < pcs.length; i++) { - const coin = await tx.coins.get(pcs[i].coinPub); - if (!coin) { - throw Error("can't calculate payment cost, coin not found"); - } - const denom = await tx.denominations.get([ - coin.exchangeBaseUrl, - coin.denomPubHash, - ]); - if (!denom) { - throw Error( - "can't calculate payment cost, denomination for coin not found", - ); - } - const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl - .iter(coin.exchangeBaseUrl) - .filter((x) => - Amounts.isSameCurrency( - DenominationRecord.getValue(x), - pcs[i].contribution, - ), - ); - const amountLeft = Amounts.sub( - DenominationRecord.getValue(denom), - pcs[i].contribution, - ).amount; - const refreshCost = getTotalRefreshCost( - allDenoms, - DenominationRecord.toDenomInfo(denom), - amountLeft, - ws.config.testing.denomselAllowLate, - ); - costs.push(Amounts.parseOrThrow(pcs[i].contribution)); - costs.push(refreshCost); - } - const zero = Amounts.zeroOfAmount(pcs[0].contribution); - return Amounts.sum([zero, ...costs]).amount; - }); -} - -export async function checkPeerPushDebit( - ws: InternalWalletState, - req: CheckPeerPushDebitRequest, -): Promise<CheckPeerPushDebitResponse> { - const instructedAmount = Amounts.parseOrThrow(req.amount); - const coinSelRes = await selectPeerCoins(ws, instructedAmount); - if (coinSelRes.type === "failure") { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, - { - insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, - }, - ); - } - const totalAmount = await getTotalPeerPaymentCost( - ws, - coinSelRes.result.coins, - ); - return { - amountEffective: Amounts.stringify(totalAmount), - amountRaw: req.amount, - }; -} - -async function processPeerPushDebitCreateReserve( - ws: InternalWalletState, - peerPushInitiation: PeerPushPaymentInitiationRecord, -): Promise<OperationAttemptResult> { - const pursePub = peerPushInitiation.pursePub; - const purseExpiration = peerPushInitiation.purseExpiration; - const hContractTerms = peerPushInitiation.contractTermsHash; - - const purseSigResp = await ws.cryptoApi.signPurseCreation({ - hContractTerms, - mergePub: peerPushInitiation.mergePub, - minAge: 0, - purseAmount: peerPushInitiation.amount, - purseExpiration, - pursePriv: peerPushInitiation.pursePriv, - }); - - const coins = await queryCoinInfosForSelection( - ws, - peerPushInitiation.coinSel, - ); - - const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ - exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, - pursePub: peerPushInitiation.pursePub, - coins, - }); - - const econtractResp = await ws.cryptoApi.encryptContractForMerge({ - contractTerms: peerPushInitiation.contractTerms, - mergePriv: peerPushInitiation.mergePriv, - pursePriv: peerPushInitiation.pursePriv, - pursePub: peerPushInitiation.pursePub, - contractPriv: peerPushInitiation.contractPriv, - contractPub: peerPushInitiation.contractPub, - }); - - const createPurseUrl = new URL( - `purses/${peerPushInitiation.pursePub}/create`, - peerPushInitiation.exchangeBaseUrl, - ); - - 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 !== HttpStatusCode.Ok) { - throw Error("got error response from exchange"); - } - - await ws.db - .mktx((x) => [x.peerPushPaymentInitiations]) - .runReadWrite(async (tx) => { - const ppi = await tx.peerPushPaymentInitiations.get(pursePub); - if (!ppi) { - return; - } - ppi.status = PeerPushPaymentInitiationStatus.Done; - await tx.peerPushPaymentInitiations.put(ppi); - }); - - return { - type: OperationAttemptResultType.Finished, - result: undefined, - }; -} - -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. - */ -export async function initiatePeerPushDebit( - ws: InternalWalletState, - req: InitiatePeerPushDebitRequest, -): Promise<InitiatePeerPushDebitResponse> { - const instructedAmount = Amounts.parseOrThrow( - req.partialContractTerms.amount, - ); - const purseExpiration = req.partialContractTerms.purse_expiration; - const contractTerms = req.partialContractTerms; - - const pursePair = await ws.cryptoApi.createEddsaKeypair({}); - const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - - const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - - const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({}); - - const coinSelRes = await selectPeerCoins(ws, instructedAmount); - - if (coinSelRes.type !== "success") { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, - { - insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, - }, - ); - } - - const sel = coinSelRes.result; - - logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`); - - const totalAmount = await getTotalPeerPaymentCost( - ws, - coinSelRes.result.coins, - ); - - await ws.db - .mktx((x) => [ - x.exchanges, - x.contractTerms, - x.coins, - x.coinAvailability, - x.denominations, - x.refreshGroups, - x.peerPushPaymentInitiations, - ]) - .runReadWrite(async (tx) => { - // FIXME: Instead of directly doing a spendCoin here, - // we might want to mark the coins as used and spend them - // after we've been able to create the purse. - await spendCoins(ws, tx, { - // allocationId: `txn:peer-push-debit:${pursePair.pub}`, - allocationId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: pursePair.pub, - }), - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayPeerPush, - }); - - await tx.peerPushPaymentInitiations.add({ - amount: Amounts.stringify(instructedAmount), - contractPriv: contractKeyPair.priv, - contractPub: contractKeyPair.pub, - contractTermsHash: hContractTerms, - exchangeBaseUrl: sel.exchangeBaseUrl, - mergePriv: mergePair.priv, - mergePub: mergePair.pub, - purseExpiration: purseExpiration, - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - timestampCreated: TalerPreciseTimestamp.now(), - status: PeerPushPaymentInitiationStatus.PendingCreatePurse, - contractTerms: contractTerms, - coinSel: { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - }, - totalCost: Amounts.stringify(totalAmount), - }); - - await tx.contractTerms.put({ - h: hContractTerms, - contractTermsRaw: contractTerms, - }); - }); - - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub: pursePair.pub, - }); - - await runOperationWithErrorReporting(ws, taskId, async () => { - return await processPeerPushDebit(ws, pursePair.pub); - }); - - return { - contractPriv: contractKeyPair.priv, - mergePriv: mergePair.priv, - pursePub: pursePair.pub, - exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, - talerUri: constructPayPushUri({ - exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, - contractPriv: contractKeyPair.priv, - }), - 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( - 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 function talerPaytoFromExchangeReserve( - exchangeBaseUrl: string, - reservePub: string, -): string { - const url = new URL(exchangeBaseUrl); - let proto: string; - if (url.protocol === "http:") { - proto = "taler-reserve-http"; - } else if (url.protocol === "https:") { - proto = "taler-reserve"; - } else { - throw Error(`unsupported exchange base URL protocol (${url.protocol})`); - } - - let path = url.pathname; - if (!path.endsWith("/")) { - path = path + "/"; - } - - return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; -} - -async function getMergeReserveInfo( - ws: InternalWalletState, - req: { - exchangeBaseUrl: string; - }, -): Promise<ReserveRecord> { - // We have to eagerly create the key pair outside of the transaction, - // due to the async crypto API. - const newReservePair = await ws.cryptoApi.createEddsaKeypair({}); - - const mergeReserveRecord: ReserveRecord = await ws.db - .mktx((x) => [x.exchanges, x.reserves, x.withdrawalGroups]) - .runReadWrite(async (tx) => { - const ex = await tx.exchanges.get(req.exchangeBaseUrl); - checkDbInvariant(!!ex); - if (ex.currentMergeReserveRowId != null) { - const reserve = await tx.reserves.get(ex.currentMergeReserveRowId); - checkDbInvariant(!!reserve); - return reserve; - } - const reserve: ReserveRecord = { - reservePriv: newReservePair.priv, - reservePub: newReservePair.pub, - }; - const insertResp = await tx.reserves.put(reserve); - checkDbInvariant(typeof insertResp.key === "number"); - reserve.rowId = insertResp.key; - ex.currentMergeReserveRowId = reserve.rowId; - await tx.exchanges.put(ex); - return reserve; - }); - - return mergeReserveRecord; -} - -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 confirmPeerPullDebit( - ws: InternalWalletState, - req: ConfirmPeerPullDebitRequest, -): Promise<AcceptPeerPullPaymentResponse> { - const peerPullInc = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadOnly(async (tx) => { - return tx.peerPullPaymentIncoming.get(req.peerPullPaymentIncomingId); - }); - - if (!peerPullInc) { - throw Error( - `can't accept unknown incoming p2p pull payment (${req.peerPullPaymentIncomingId})`, - ); - } - - const instructedAmount = Amounts.parseOrThrow( - peerPullInc.contractTerms.amount, - ); - - const coinSelRes = await selectPeerCoins(ws, instructedAmount); - logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); - - if (coinSelRes.type !== "success") { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, - { - insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, - }, - ); - } - - const sel = coinSelRes.result; - - const totalAmount = await getTotalPeerPaymentCost( - ws, - coinSelRes.result.coins, - ); - - const ppi = await ws.db - .mktx((x) => [ - x.exchanges, - x.coins, - x.denominations, - x.refreshGroups, - x.peerPullPaymentIncoming, - x.coinAvailability, - ]) - .runReadWrite(async (tx) => { - await spendCoins(ws, tx, { - // allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`, - allocationId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, - }), - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayPeerPull, - }); - - const pi = await tx.peerPullPaymentIncoming.get( - req.peerPullPaymentIncomingId, - ); - if (!pi) { - throw Error(); - } - if (pi.status === PeerPullDebitRecordStatus.DialogProposed) { - pi.status = PeerPullDebitRecordStatus.PendingDeposit; - pi.coinSel = { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - totalCost: Amounts.stringify(totalAmount), - }; - } - await tx.peerPullPaymentIncoming.put(pi); - return pi; - }); - - await runOperationWithErrorReporting( - ws, - TaskIdentifiers.forPeerPullPaymentDebit(ppi), - async () => { - return processPeerPullDebit(ws, ppi.peerPullPaymentIncomingId); - }, - ); - - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, - }); - - return { - transactionId, - }; -} - -/** - * Look up information about an incoming peer pull payment. - * Store the results in the wallet DB. - */ -export async function preparePeerPullDebit( - ws: InternalWalletState, - req: PreparePeerPullDebitRequest, -): Promise<PreparePeerPullDebitResponse> { - const uri = parsePayPullUri(req.talerUri); - - if (!uri) { - throw Error("got invalid taler://pay-pull URI"); - } - - const existingPullIncomingRecord = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadOnly(async (tx) => { - return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([ - uri.exchangeBaseUrl, - uri.contractPriv, - ]); - }); - - if (existingPullIncomingRecord) { - return { - amount: existingPullIncomingRecord.contractTerms.amount, - amountRaw: existingPullIncomingRecord.contractTerms.amount, - amountEffective: existingPullIncomingRecord.totalCostEstimated, - contractTerms: existingPullIncomingRecord.contractTerms, - peerPullPaymentIncomingId: - existingPullIncomingRecord.peerPullPaymentIncomingId, - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: - existingPullIncomingRecord.peerPullPaymentIncomingId, - }), - }; - } - - const exchangeBaseUrl = uri.exchangeBaseUrl; - const contractPriv = uri.contractPriv; - const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); - - const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); - - const contractHttpResp = await ws.http.get(getContractUrl.href); - - const contractResp = await readSuccessResponseJsonOrThrow( - contractHttpResp, - codecForExchangeGetContractResponse(), - ); - - const pursePub = contractResp.purse_pub; - - const dec = await ws.cryptoApi.decryptContractForDeposit({ - ciphertext: contractResp.econtract, - contractPriv: contractPriv, - pursePub: pursePub, - }); - - const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl); - - const purseHttpResp = await ws.http.get(getPurseUrl.href); - - const purseStatus = await readSuccessResponseJsonOrThrow( - purseHttpResp, - codecForExchangePurseStatus(), - ); - - const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32)); - - let contractTerms: PeerContractTerms; - - if (dec.contractTerms) { - contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); - // FIXME: Check that the purseStatus balance matches contract terms amount - } else { - // FIXME: In this case, where do we get the purse expiration from?! - // https://bugs.gnunet.org/view.php?id=7706 - throw Error("pull payments without contract terms not supported yet"); - } - - // FIXME: Why don't we compute the totalCost here?! - - const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); - - const coinSelRes = await selectPeerCoins(ws, instructedAmount); - logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); - - if (coinSelRes.type !== "success") { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, - { - insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, - }, - ); - } - - const totalAmount = await getTotalPeerPaymentCost( - ws, - coinSelRes.result.coins, - ); - - await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadWrite(async (tx) => { - await tx.peerPullPaymentIncoming.add({ - peerPullPaymentIncomingId, - contractPriv: contractPriv, - exchangeBaseUrl: exchangeBaseUrl, - pursePub: pursePub, - timestampCreated: TalerPreciseTimestamp.now(), - contractTerms, - status: PeerPullDebitRecordStatus.DialogProposed, - totalCostEstimated: Amounts.stringify(totalAmount), - }); - }); - - return { - amount: contractTerms.amount, - amountEffective: Amounts.stringify(totalAmount), - amountRaw: contractTerms.amount, - contractTerms: contractTerms, - peerPullPaymentIncomingId, - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId: peerPullPaymentIncomingId, - }), - }; -} - -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, - }; -} - -/** - * 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; -} - -/** - * 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, - }; -} - -/** - * 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 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, - }; - case PeerPushPaymentInitiationStatus.Failed: - return { - major: TransactionMajorState.Failed, - }; - } -} - -export function computePeerPushDebitTransactionActions( - ppiRecord: PeerPushPaymentInitiationRecord, -): TransactionAction[] { - switch (ppiRecord.status) { - case PeerPushPaymentInitiationStatus.PendingCreatePurse: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushPaymentInitiationStatus.PendingReady: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushPaymentInitiationStatus.Aborted: - return [TransactionAction.Delete]; - case PeerPushPaymentInitiationStatus.AbortingDeletePurse: - return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.AbortingRefresh: - return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: - return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPushPaymentInitiationStatus.SuspendedReady: - return [TransactionAction.Suspend, TransactionAction.Abort]; - case PeerPushPaymentInitiationStatus.Done: - return [TransactionAction.Delete]; - case PeerPushPaymentInitiationStatus.Failed: - return [TransactionAction.Delete]; - } -} - -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; - case PeerPushPaymentInitiationStatus.Failed: - 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 failPeerPushDebitTransaction( - 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: - case PeerPushPaymentInitiationStatus.Failed: - // 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: - case PeerPushPaymentInitiationStatus.Failed: - // 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 suspendPeerPullDebitTransaction( - ws: InternalWalletState, - peerPullPaymentIncomingId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - break; - case PeerPullDebitRecordStatus.DonePaid: - break; - case PeerPullDebitRecordStatus.PendingDeposit: - newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.AbortingRefresh: - newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh; - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortPeerPullDebitTransaction( - ws: InternalWalletState, - peerPullPaymentIncomingId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - newStatus = PeerPullDebitRecordStatus.Aborted; - break; - case PeerPullDebitRecordStatus.DonePaid: - break; - case PeerPullDebitRecordStatus.PendingDeposit: - newStatus = PeerPullDebitRecordStatus.AbortingRefresh; - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.AbortingRefresh: - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPullDebitTransaction( - ws: InternalWalletState, - peerPullPaymentIncomingId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - newStatus = PeerPullDebitRecordStatus.Aborted; - break; - case PeerPullDebitRecordStatus.DonePaid: - break; - case PeerPullDebitRecordStatus.PendingDeposit: - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - case PeerPullDebitRecordStatus.AbortingRefresh: - // FIXME: abort underlying refresh! - newStatus = PeerPullDebitRecordStatus.Failed; - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPullDebitTransaction( - ws: InternalWalletState, - peerPullPaymentIncomingId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullPaymentIncomingId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullPaymentIncoming]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullPaymentIncoming.get( - peerPullPaymentIncomingId, - ); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - case PeerPullDebitRecordStatus.DonePaid: - case PeerPullDebitRecordStatus.PendingDeposit: - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - newStatus = PeerPullDebitRecordStatus.PendingDeposit; - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.AbortingRefresh: - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - newStatus = PeerPullDebitRecordStatus.AbortingRefresh; - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullPaymentIncoming.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - -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 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 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: - case PeerPushPaymentInitiationStatus.Failed: - // 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; - }); - 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); - } -} - -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]; - } -} - -export function computePeerPullDebitTransactionState( - pullDebitRecord: PeerPullPaymentIncomingRecord, -): TransactionState { - switch (pullDebitRecord.status) { - case PeerPullDebitRecordStatus.DialogProposed: - return { - major: TransactionMajorState.Dialog, - minor: TransactionMinorState.Proposed, - }; - case PeerPullDebitRecordStatus.PendingDeposit: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Deposit, - }; - case PeerPullDebitRecordStatus.DonePaid: - return { - major: TransactionMajorState.Done, - }; - case PeerPullDebitRecordStatus.SuspendedDeposit: - return { - major: TransactionMajorState.Suspended, - minor: TransactionMinorState.Deposit, - }; - case PeerPullDebitRecordStatus.Aborted: - return { - major: TransactionMajorState.Aborted, - }; - case PeerPullDebitRecordStatus.AbortingRefresh: - return { - major: TransactionMajorState.Aborting, - minor: TransactionMinorState.Refresh, - }; - case PeerPullDebitRecordStatus.Failed: - return { - major: TransactionMajorState.Failed, - }; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - return { - major: TransactionMajorState.SuspendedAborting, - minor: TransactionMinorState.Refresh, - }; - } -} - -export function computePeerPullDebitTransactionActions( - pullDebitRecord: PeerPullPaymentIncomingRecord, -): TransactionAction[] { - switch (pullDebitRecord.status) { - case PeerPullDebitRecordStatus.DialogProposed: - return []; - case PeerPullDebitRecordStatus.PendingDeposit: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullDebitRecordStatus.DonePaid: - return [TransactionAction.Delete]; - case PeerPullDebitRecordStatus.SuspendedDeposit: - return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPullDebitRecordStatus.Aborted: - return [TransactionAction.Delete]; - case PeerPullDebitRecordStatus.AbortingRefresh: - return [TransactionAction.Fail, TransactionAction.Suspend]; - case PeerPullDebitRecordStatus.Failed: - return [TransactionAction.Delete]; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - return [TransactionAction.Resume, TransactionAction.Fail]; - } -} |