diff options
author | Florian Dold <florian@dold.me> | 2023-06-05 11:45:16 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-06-05 11:45:16 +0200 |
commit | fda5a0ed87a6473a6b34bd1ac07d5f1d45dfbc19 (patch) | |
tree | c8b7b09ca441d2a01e340dd3f569e075d3ef278e /packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts | |
parent | f3d4ff4e3a44141ad387ef68a9083b01bf1c818a (diff) | |
download | wallet-core-fda5a0ed87a6473a6b34bd1ac07d5f1d45dfbc19.tar.xz |
wallet-core: restructure p2p implv0.9.3-dev.14
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts | 604 |
1 files changed, 604 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts new file mode 100644 index 000000000..fdec42bbd --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -0,0 +1,604 @@ +/* + This file is part of GNU Taler + (C) 2022-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + ConfirmPeerPullDebitRequest, + AcceptPeerPullPaymentResponse, + Amounts, + j2s, + TalerError, + TalerErrorCode, + TransactionType, + RefreshReason, + Logger, + PeerContractTerms, + PreparePeerPullDebitRequest, + PreparePeerPullDebitResponse, + TalerPreciseTimestamp, + codecForExchangeGetContractResponse, + codecForPeerContractTerms, + decodeCrock, + eddsaGetPublic, + encodeCrock, + getRandomBytes, + parsePayPullUri, + TransactionAction, + TransactionMajorState, + TransactionMinorState, + TransactionState, +} from "@gnu-taler/taler-util"; +import { + InternalWalletState, + PeerPullDebitRecordStatus, + PeerPullPaymentIncomingRecord, + PendingTaskType, +} from "../index.js"; +import { TaskIdentifiers, constructTaskIdentifier } from "../util/retries.js"; +import { spendCoins, runOperationWithErrorReporting } from "./common.js"; +import { + codecForExchangePurseStatus, + getTotalPeerPaymentCost, + selectPeerCoins, +} from "./pay-peer-common.js"; +import { processPeerPullDebit } from "./pay-peer-push-credit.js"; +import { + constructTransactionIdentifier, + notifyTransition, + stopLongpolling, +} from "./transactions.js"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { assertUnreachable } from "../util/assertUnreachable.js"; + +const logger = new Logger("pay-peer-pull-debit.ts"); + +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 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 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]; + } +} |