diff options
author | Florian Dold <florian@dold.me> | 2024-02-19 18:05:48 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-02-19 18:05:48 +0100 |
commit | e951075d2ef52fa8e9e7489c62031777c3a7e66b (patch) | |
tree | 64208c09a9162f3a99adccf30edc36de1ef884ef /packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts | |
parent | e975740ac4e9ba4bc531226784d640a018c00833 (diff) | |
download | wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.gz wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.bz2 wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.zip |
wallet-core: flatten directory structure
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts | 1150 |
1 files changed, 0 insertions, 1150 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts deleted file mode 100644 index 91c5430be..000000000 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ /dev/null @@ -1,1150 +0,0 @@ -/* - 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 { - Amounts, - CancellationToken, - CheckPeerPushDebitRequest, - CheckPeerPushDebitResponse, - CoinRefreshRequest, - ContractTermsUtil, - HttpStatusCode, - InitiatePeerPushDebitRequest, - InitiatePeerPushDebitResponse, - Logger, - NotificationType, - RefreshReason, - TalerError, - TalerErrorCode, - TalerPreciseTimestamp, - TalerProtocolTimestamp, - TalerProtocolViolationError, - TransactionAction, - TransactionIdStr, - TransactionMajorState, - TransactionMinorState, - TransactionState, - TransactionType, - encodeCrock, - getRandomBytes, - j2s, -} from "@gnu-taler/taler-util"; -import { - HttpResponse, - readSuccessResponseJsonOrThrow, - readTalerErrorResponse, -} from "@gnu-taler/taler-util/http"; -import { EncryptContractRequest } from "../crypto/cryptoTypes.js"; -import { - PeerPushDebitRecord, - PeerPushDebitStatus, - RefreshOperationStatus, - createRefreshGroup, - timestampPreciseToDb, - timestampProtocolFromDb, - timestampProtocolToDb, -} from "../index.js"; -import { InternalWalletState } from "../internal-wallet-state.js"; -import { PendingTaskType, TaskId } from "../pending-types.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; -import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js"; -import { checkLogicInvariant } from "../util/invariants.js"; -import { - TaskRunResult, - TaskRunResultType, - TransactionContext, - constructTaskIdentifier, - spendCoins, -} from "./common.js"; -import { - codecForExchangePurseStatus, - getTotalPeerPaymentCost, - queryCoinInfosForSelection, -} from "./pay-peer-common.js"; -import { - constructTransactionIdentifier, - notifyTransition, -} from "./transactions.js"; - -const logger = new Logger("pay-peer-push-debit.ts"); - -export class PeerPushDebitTransactionContext implements TransactionContext { - readonly transactionId: TransactionIdStr; - readonly retryTag: TaskId; - - constructor( - public ws: InternalWalletState, - public pursePub: string, - ) { - this.transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - this.retryTag = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); - } - - async deleteTransaction(): Promise<void> { - const { ws, pursePub, transactionId } = this; - await ws.db.runReadWriteTx(["peerPushDebit", "tombstones"], async (tx) => { - const debit = await tx.peerPushDebit.get(pursePub); - if (debit) { - await tx.peerPushDebit.delete(pursePub); - await tx.tombstones.put({ id: transactionId }); - } - }); - } - - async suspendTransaction(): Promise<void> { - const { ws, pursePub, transactionId, retryTag } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPushDebit"], - async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.PendingCreatePurse: - newStatus = PeerPushDebitStatus.SuspendedCreatePurse; - break; - case PeerPushDebitStatus.AbortingRefreshDeleted: - newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted; - break; - case PeerPushDebitStatus.AbortingRefreshExpired: - newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired; - break; - case PeerPushDebitStatus.AbortingDeletePurse: - newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse; - break; - case PeerPushDebitStatus.PendingReady: - newStatus = PeerPushDebitStatus.SuspendedReady; - break; - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - case PeerPushDebitStatus.SuspendedReady: - case PeerPushDebitStatus.SuspendedCreatePurse: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Expired: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - ws.taskScheduler.stopShepherdTask(retryTag); - notifyTransition(ws, transactionId, transitionInfo); - } - - async abortTransaction(): Promise<void> { - const { ws, pursePub, transactionId, retryTag } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPushDebit"], - async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.SuspendedReady: - newStatus = PeerPushDebitStatus.AbortingDeletePurse; - break; - case PeerPushDebitStatus.SuspendedCreatePurse: - case PeerPushDebitStatus.PendingCreatePurse: - // Network request might already be in-flight! - newStatus = PeerPushDebitStatus.AbortingDeletePurse; - break; - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - case PeerPushDebitStatus.AbortingRefreshDeleted: - case PeerPushDebitStatus.AbortingRefreshExpired: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.AbortingDeletePurse: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Expired: - case PeerPushDebitStatus.Failed: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - ws.taskScheduler.stopShepherdTask(retryTag); - notifyTransition(ws, transactionId, transitionInfo); - ws.taskScheduler.startShepherdTask(retryTag); - } - - async resumeTransaction(): Promise<void> { - const { ws, pursePub, transactionId, retryTag } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPushDebit"], - async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPushDebitStatus.AbortingDeletePurse; - break; - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - newStatus = PeerPushDebitStatus.AbortingRefreshDeleted; - break; - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - newStatus = PeerPushDebitStatus.AbortingRefreshExpired; - break; - case PeerPushDebitStatus.SuspendedReady: - newStatus = PeerPushDebitStatus.PendingReady; - break; - case PeerPushDebitStatus.SuspendedCreatePurse: - newStatus = PeerPushDebitStatus.PendingCreatePurse; - break; - case PeerPushDebitStatus.PendingCreatePurse: - case PeerPushDebitStatus.AbortingRefreshDeleted: - case PeerPushDebitStatus.AbortingRefreshExpired: - case PeerPushDebitStatus.AbortingDeletePurse: - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Expired: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - ws.taskScheduler.startShepherdTask(retryTag); - notifyTransition(ws, transactionId, transitionInfo); - } - - async failTransaction(): Promise<void> { - const { ws, pursePub, transactionId, retryTag } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPushDebit"], - async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.AbortingRefreshDeleted: - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - // FIXME: What to do about the refresh group? - newStatus = PeerPushDebitStatus.Failed; - break; - case PeerPushDebitStatus.AbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.AbortingRefreshExpired: - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.SuspendedReady: - case PeerPushDebitStatus.SuspendedCreatePurse: - case PeerPushDebitStatus.PendingCreatePurse: - newStatus = PeerPushDebitStatus.Failed; - break; - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Expired: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - ws.taskScheduler.stopShepherdTask(retryTag); - notifyTransition(ws, transactionId, transitionInfo); - ws.taskScheduler.startShepherdTask(retryTag); - } -} - -export async function checkPeerPushDebit( - ws: InternalWalletState, - req: CheckPeerPushDebitRequest, -): Promise<CheckPeerPushDebitResponse> { - const instructedAmount = Amounts.parseOrThrow(req.amount); - logger.trace( - `checking peer push debit for ${Amounts.stringify(instructedAmount)}`, - ); - const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); - if (coinSelRes.type === "failure") { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, - { - insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, - }, - ); - } - logger.trace(`selected peer coins (len=${coinSelRes.result.coins.length})`); - const totalAmount = await getTotalPeerPaymentCost( - ws, - coinSelRes.result.coins, - ); - logger.trace("computed total peer payment cost"); - return { - exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, - amountEffective: Amounts.stringify(totalAmount), - amountRaw: req.amount, - maxExpirationDate: coinSelRes.result.maxExpirationDate, - }; -} - -async function handlePurseCreationConflict( - ws: InternalWalletState, - peerPushInitiation: PeerPushDebitRecord, - resp: HttpResponse, -): Promise<TaskRunResult> { - const pursePub = peerPushInitiation.pursePub; - const errResp = await readTalerErrorResponse(resp); - const ctx = new PeerPushDebitTransactionContext(ws, pursePub); - if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { - await ctx.failTransaction(); - return TaskRunResult.finished(); - } - - // FIXME: Properly parse! - const brokenCoinPub = (errResp as any).coin_pub; - logger.trace(`excluded broken coin pub=${brokenCoinPub}`); - - if (!brokenCoinPub) { - // FIXME: Details! - throw new TalerProtocolViolationError(); - } - - const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount); - const sel = peerPushInitiation.coinSel; - - const repair: PeerCoinRepair = { - coinPubs: [], - contribs: [], - exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, - }; - - for (let i = 0; i < sel.coinPubs.length; i++) { - if (sel.coinPubs[i] != brokenCoinPub) { - repair.coinPubs.push(sel.coinPubs[i]); - repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i])); - } - } - - const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair }); - - if (coinSelRes.type == "failure") { - // FIXME: Details! - throw Error( - "insufficient balance to re-select coins to repair double spending", - ); - } - - await ws.db.runReadWriteTx(["peerPushDebit"], async (tx) => { - const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub); - if (!myPpi) { - return; - } - switch (myPpi.status) { - case PeerPushDebitStatus.PendingCreatePurse: - case PeerPushDebitStatus.SuspendedCreatePurse: { - const sel = coinSelRes.result; - myPpi.coinSel = { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - }; - break; - } - default: - return; - } - await tx.peerPushDebit.put(myPpi); - }); - return TaskRunResult.progress(); -} - -async function processPeerPushDebitCreateReserve( - ws: InternalWalletState, - peerPushInitiation: PeerPushDebitRecord, -): Promise<TaskRunResult> { - const pursePub = peerPushInitiation.pursePub; - const purseExpiration = peerPushInitiation.purseExpiration; - const hContractTerms = peerPushInitiation.contractTermsHash; - const ctx = new PeerPushDebitTransactionContext(ws, pursePub); - const transactionId = ctx.transactionId; - - logger.trace(`processing ${transactionId} pending(create-reserve)`); - - const contractTermsRecord = await ws.db.runReadOnlyTx( - ["contractTerms"], - async (tx) => { - return tx.contractTerms.get(hContractTerms); - }, - ); - - if (!contractTermsRecord) { - throw Error( - `db invariant failed, contract terms for ${transactionId} missing`, - ); - } - - const purseSigResp = await ws.cryptoApi.signPurseCreation({ - hContractTerms, - mergePub: peerPushInitiation.mergePub, - minAge: 0, - purseAmount: peerPushInitiation.amount, - purseExpiration: timestampProtocolFromDb(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 encryptContractRequest: EncryptContractRequest = { - contractTerms: contractTermsRecord.contractTermsRaw, - mergePriv: peerPushInitiation.mergePriv, - pursePriv: peerPushInitiation.pursePriv, - pursePub: peerPushInitiation.pursePub, - contractPriv: peerPushInitiation.contractPriv, - contractPub: peerPushInitiation.contractPub, - nonce: peerPushInitiation.contractEncNonce, - }; - - logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`); - - const econtractResp = await ws.cryptoApi.encryptContractForMerge( - encryptContractRequest, - ); - - const createPurseUrl = new URL( - `purses/${peerPushInitiation.pursePub}/create`, - peerPushInitiation.exchangeBaseUrl, - ); - - const reqBody = { - amount: peerPushInitiation.amount, - merge_pub: peerPushInitiation.mergePub, - purse_sig: purseSigResp.sig, - h_contract_terms: hContractTerms, - purse_expiration: timestampProtocolFromDb(purseExpiration), - deposits: depositSigsResp.deposits, - min_age: 0, - econtract: econtractResp.econtract, - }; - - logger.trace(`request body: ${j2s(reqBody)}`); - - const httpResp = await ws.http.fetch(createPurseUrl.href, { - method: "POST", - body: reqBody, - }); - - { - const resp = await httpResp.json(); - logger.info(`resp: ${j2s(resp)}`); - } - - switch (httpResp.status) { - case HttpStatusCode.Ok: - break; - case HttpStatusCode.Forbidden: { - // FIXME: Store this error! - await ctx.failTransaction(); - return TaskRunResult.finished(); - } - case HttpStatusCode.Conflict: { - // Handle double-spending - return handlePurseCreationConflict(ws, peerPushInitiation, httpResp); - } - default: { - const errResp = await readTalerErrorResponse(httpResp); - return { - type: TaskRunResultType.Error, - errorDetail: errResp, - }; - } - } - - if (httpResp.status !== HttpStatusCode.Ok) { - // FIXME: do proper error reporting - throw Error("got error response from exchange"); - } - - await transitionPeerPushDebitTransaction(ws, pursePub, { - stFrom: PeerPushDebitStatus.PendingCreatePurse, - stTo: PeerPushDebitStatus.PendingReady, - }); - - return TaskRunResult.backoff(); -} - -async function processPeerPushDebitAbortingDeletePurse( - ws: InternalWalletState, - peerPushInitiation: PeerPushDebitRecord, -): Promise<TaskRunResult> { - const { pursePub, pursePriv } = peerPushInitiation; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - - const sigResp = await ws.cryptoApi.signDeletePurse({ - pursePriv, - }); - const purseUrl = new URL( - `purses/${pursePub}`, - peerPushInitiation.exchangeBaseUrl, - ); - const resp = await ws.http.fetch(purseUrl.href, { - method: "DELETE", - headers: { - "taler-purse-signature": sigResp.sig, - }, - }); - logger.info(`deleted purse with response status ${resp.status}`); - - const transitionInfo = await ws.db.runReadWriteTx( - [ - "peerPushDebit", - "refreshGroups", - "denominations", - "coinAvailability", - "coins", - ], - async (tx) => { - const ppiRec = await tx.peerPushDebit.get(pursePub); - if (!ppiRec) { - return undefined; - } - if (ppiRec.status !== PeerPushDebitStatus.AbortingDeletePurse) { - return undefined; - } - const currency = Amounts.currencyOf(ppiRec.amount); - const oldTxState = computePeerPushDebitTransactionState(ppiRec); - const coinPubs: CoinRefreshRequest[] = []; - - for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) { - coinPubs.push({ - amount: ppiRec.coinSel.contributions[i], - coinPub: ppiRec.coinSel.coinPubs[i], - }); - } - - const refresh = await createRefreshGroup( - ws, - tx, - currency, - coinPubs, - RefreshReason.AbortPeerPushDebit, - transactionId, - ); - ppiRec.status = PeerPushDebitStatus.AbortingRefreshDeleted; - ppiRec.abortRefreshGroupId = refresh.refreshGroupId; - await tx.peerPushDebit.put(ppiRec); - const newTxState = computePeerPushDebitTransactionState(ppiRec); - return { - oldTxState, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - - return TaskRunResult.backoff(); -} - -interface SimpleTransition { - stFrom: PeerPushDebitStatus; - stTo: PeerPushDebitStatus; -} - -async function transitionPeerPushDebitTransaction( - ws: InternalWalletState, - pursePub: string, - transitionSpec: SimpleTransition, -): Promise<void> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPushDebit"], - async (tx) => { - const ppiRec = await tx.peerPushDebit.get(pursePub); - if (!ppiRec) { - return undefined; - } - if (ppiRec.status !== transitionSpec.stFrom) { - return undefined; - } - const oldTxState = computePeerPushDebitTransactionState(ppiRec); - ppiRec.status = transitionSpec.stTo; - await tx.peerPushDebit.put(ppiRec); - const newTxState = computePeerPushDebitTransactionState(ppiRec); - return { - oldTxState, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); -} - -async function processPeerPushDebitAbortingRefreshDeleted( - ws: InternalWalletState, - peerPushInitiation: PeerPushDebitRecord, -): Promise<TaskRunResult> { - const pursePub = peerPushInitiation.pursePub; - const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId; - checkLogicInvariant(!!abortRefreshGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: peerPushInitiation.pursePub, - }); - const transitionInfo = await ws.db.runReadWriteTx( - ["refreshGroups", "peerPushDebit"], - async (tx) => { - const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); - let newOpState: PeerPushDebitStatus | undefined; - if (!refreshGroup) { - // Maybe it got manually deleted? Means that we should - // just go into failed. - logger.warn("no aborting refresh group found for deposit group"); - newOpState = PeerPushDebitStatus.Failed; - } else { - if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) { - newOpState = PeerPushDebitStatus.Aborted; - } else if ( - refreshGroup.operationStatus === RefreshOperationStatus.Failed - ) { - newOpState = PeerPushDebitStatus.Failed; - } - } - if (newOpState) { - const newDg = await tx.peerPushDebit.get(pursePub); - if (!newDg) { - return; - } - const oldTxState = computePeerPushDebitTransactionState(newDg); - newDg.status = newOpState; - const newTxState = computePeerPushDebitTransactionState(newDg); - await tx.peerPushDebit.put(newDg); - return { oldTxState, newTxState }; - } - return undefined; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - // FIXME: Shouldn't this be finished in some cases?! - return TaskRunResult.backoff(); -} - -async function processPeerPushDebitAbortingRefreshExpired( - ws: InternalWalletState, - peerPushInitiation: PeerPushDebitRecord, -): Promise<TaskRunResult> { - const pursePub = peerPushInitiation.pursePub; - const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId; - checkLogicInvariant(!!abortRefreshGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: peerPushInitiation.pursePub, - }); - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPushDebit", "refreshGroups"], - async (tx) => { - const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); - let newOpState: PeerPushDebitStatus | undefined; - if (!refreshGroup) { - // Maybe it got manually deleted? Means that we should - // just go into failed. - logger.warn("no aborting refresh group found for deposit group"); - newOpState = PeerPushDebitStatus.Failed; - } else { - if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) { - newOpState = PeerPushDebitStatus.Expired; - } else if ( - refreshGroup.operationStatus === RefreshOperationStatus.Failed - ) { - newOpState = PeerPushDebitStatus.Failed; - } - } - if (newOpState) { - const newDg = await tx.peerPushDebit.get(pursePub); - if (!newDg) { - return; - } - const oldTxState = computePeerPushDebitTransactionState(newDg); - newDg.status = newOpState; - const newTxState = computePeerPushDebitTransactionState(newDg); - await tx.peerPushDebit.put(newDg); - return { oldTxState, newTxState }; - } - return undefined; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - // FIXME: Shouldn't this be finished in some cases?! - return TaskRunResult.backoff(); -} - -/** - * Process the "pending(ready)" state of a peer-push-debit transaction. - */ -async function processPeerPushDebitReady( - ws: InternalWalletState, - peerPushInitiation: PeerPushDebitRecord, - cancellationToken: CancellationToken, -): Promise<TaskRunResult> { - logger.trace("processing peer-push-debit pending(ready)"); - const pursePub = peerPushInitiation.pursePub; - const transactionId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); - const mergeUrl = new URL( - `purses/${pursePub}/merge`, - peerPushInitiation.exchangeBaseUrl, - ); - mergeUrl.searchParams.set("timeout_ms", "30000"); - logger.info(`long-polling on purse status at ${mergeUrl.href}`); - const resp = await ws.http.fetch(mergeUrl.href, { - // timeout: getReserveRequestTimeout(withdrawalGroup), - cancellationToken, - }); - if (resp.status === HttpStatusCode.Ok) { - const purseStatus = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangePurseStatus(), - ); - const mergeTimestamp = purseStatus.merge_timestamp; - logger.info(`got purse status ${j2s(purseStatus)}`); - if (!mergeTimestamp || TalerProtocolTimestamp.isNever(mergeTimestamp)) { - return TaskRunResult.backoff(); - } else { - await transitionPeerPushDebitTransaction( - ws, - peerPushInitiation.pursePub, - { - stFrom: PeerPushDebitStatus.PendingReady, - stTo: PeerPushDebitStatus.Done, - }, - ); - return TaskRunResult.finished(); - } - } else if (resp.status === HttpStatusCode.Gone) { - logger.info(`purse ${pursePub} is gone, aborting peer-push-debit`); - const transitionInfo = await ws.db.runReadWriteTx( - [ - "peerPushDebit", - "refreshGroups", - "denominations", - "coinAvailability", - "coins", - ], - async (tx) => { - const ppiRec = await tx.peerPushDebit.get(pursePub); - if (!ppiRec) { - return undefined; - } - if (ppiRec.status !== PeerPushDebitStatus.PendingReady) { - return undefined; - } - const currency = Amounts.currencyOf(ppiRec.amount); - const oldTxState = computePeerPushDebitTransactionState(ppiRec); - const coinPubs: CoinRefreshRequest[] = []; - - for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) { - coinPubs.push({ - amount: ppiRec.coinSel.contributions[i], - coinPub: ppiRec.coinSel.coinPubs[i], - }); - } - - const refresh = await createRefreshGroup( - ws, - tx, - currency, - coinPubs, - RefreshReason.AbortPeerPushDebit, - transactionId, - ); - ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired; - ppiRec.abortRefreshGroupId = refresh.refreshGroupId; - await tx.peerPushDebit.put(ppiRec); - const newTxState = computePeerPushDebitTransactionState(ppiRec); - return { - oldTxState, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - return TaskRunResult.backoff(); - } else { - logger.warn(`unexpected HTTP status for purse: ${resp.status}`); - return TaskRunResult.backoff(); - } -} - -export async function processPeerPushDebit( - ws: InternalWalletState, - pursePub: string, - cancellationToken: CancellationToken, -): Promise<TaskRunResult> { - const peerPushInitiation = await ws.db.runReadOnlyTx( - ["peerPushDebit"], - async (tx) => { - return tx.peerPushDebit.get(pursePub); - }, - ); - if (!peerPushInitiation) { - throw Error("peer push payment not found"); - } - - switch (peerPushInitiation.status) { - case PeerPushDebitStatus.PendingCreatePurse: - return processPeerPushDebitCreateReserve(ws, peerPushInitiation); - case PeerPushDebitStatus.PendingReady: - return processPeerPushDebitReady( - ws, - peerPushInitiation, - cancellationToken, - ); - case PeerPushDebitStatus.AbortingDeletePurse: - return processPeerPushDebitAbortingDeletePurse(ws, peerPushInitiation); - case PeerPushDebitStatus.AbortingRefreshDeleted: - return processPeerPushDebitAbortingRefreshDeleted(ws, peerPushInitiation); - case PeerPushDebitStatus.AbortingRefreshExpired: - return processPeerPushDebitAbortingRefreshExpired(ws, peerPushInitiation); - default: { - const txState = computePeerPushDebitTransactionState(peerPushInitiation); - logger.warn( - `not processing peer-push-debit transaction in state ${j2s(txState)}`, - ); - } - } - - return TaskRunResult.finished(); -} - -/** - * 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):`); - logger.trace(`${j2s(coinSelRes)}`); - - const totalAmount = await getTotalPeerPaymentCost( - ws, - coinSelRes.result.coins, - ); - - logger.info(`computed total peer payment cost`); - - const pursePub = pursePair.pub; - - const ctx = new PeerPushDebitTransactionContext(ws, pursePub); - - const transactionId = ctx.transactionId; - - const contractEncNonce = encodeCrock(getRandomBytes(24)); - - const transitionInfo = await ws.db.runReadWriteTx( - [ - "exchanges", - "contractTerms", - "coins", - "coinAvailability", - "denominations", - "refreshGroups", - "peerPushDebit", - ], - 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: 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, - }); - - const ppi: PeerPushDebitRecord = { - amount: Amounts.stringify(instructedAmount), - contractPriv: contractKeyPair.priv, - contractPub: contractKeyPair.pub, - contractTermsHash: hContractTerms, - exchangeBaseUrl: sel.exchangeBaseUrl, - mergePriv: mergePair.priv, - mergePub: mergePair.pub, - purseExpiration: timestampProtocolToDb(purseExpiration), - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), - status: PeerPushDebitStatus.PendingCreatePurse, - contractEncNonce, - coinSel: { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - }, - totalCost: Amounts.stringify(totalAmount), - }; - - await tx.peerPushDebit.add(ppi); - - await tx.contractTerms.put({ - h: hContractTerms, - contractTermsRaw: contractTerms, - }); - - const newTxState = computePeerPushDebitTransactionState(ppi); - return { - oldTxState: { major: TransactionMajorState.None }, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - ws.notify({ - type: NotificationType.BalanceChange, - hintTransactionId: transactionId, - }); - - ws.taskScheduler.startShepherdTask(ctx.retryTag); - - return { - contractPriv: contractKeyPair.priv, - mergePriv: mergePair.priv, - pursePub: pursePair.pub, - exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: pursePair.pub, - }), - }; -} - -export function computePeerPushDebitTransactionActions( - ppiRecord: PeerPushDebitRecord, -): TransactionAction[] { - switch (ppiRecord.status) { - case PeerPushDebitStatus.PendingCreatePurse: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushDebitStatus.PendingReady: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPushDebitStatus.Aborted: - return [TransactionAction.Delete]; - case PeerPushDebitStatus.AbortingDeletePurse: - return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushDebitStatus.AbortingRefreshDeleted: - return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushDebitStatus.AbortingRefreshExpired: - return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushDebitStatus.SuspendedCreatePurse: - return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPushDebitStatus.SuspendedReady: - return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPushDebitStatus.Done: - return [TransactionAction.Delete]; - case PeerPushDebitStatus.Expired: - return [TransactionAction.Delete]; - case PeerPushDebitStatus.Failed: - return [TransactionAction.Delete]; - } -} - -export function computePeerPushDebitTransactionState( - ppiRecord: PeerPushDebitRecord, -): TransactionState { - switch (ppiRecord.status) { - case PeerPushDebitStatus.PendingCreatePurse: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.CreatePurse, - }; - case PeerPushDebitStatus.PendingReady: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Ready, - }; - case PeerPushDebitStatus.Aborted: - return { - major: TransactionMajorState.Aborted, - }; - case PeerPushDebitStatus.AbortingDeletePurse: - return { - major: TransactionMajorState.Aborting, - minor: TransactionMinorState.DeletePurse, - }; - case PeerPushDebitStatus.AbortingRefreshDeleted: - return { - major: TransactionMajorState.Aborting, - minor: TransactionMinorState.Refresh, - }; - case PeerPushDebitStatus.AbortingRefreshExpired: - return { - major: TransactionMajorState.Aborting, - minor: TransactionMinorState.RefreshExpired, - }; - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - return { - major: TransactionMajorState.SuspendedAborting, - minor: TransactionMinorState.DeletePurse, - }; - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - return { - major: TransactionMajorState.SuspendedAborting, - minor: TransactionMinorState.RefreshExpired, - }; - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - return { - major: TransactionMajorState.SuspendedAborting, - minor: TransactionMinorState.Refresh, - }; - case PeerPushDebitStatus.SuspendedCreatePurse: - return { - major: TransactionMajorState.Suspended, - minor: TransactionMinorState.CreatePurse, - }; - case PeerPushDebitStatus.SuspendedReady: - return { - major: TransactionMajorState.Suspended, - minor: TransactionMinorState.Ready, - }; - case PeerPushDebitStatus.Done: - return { - major: TransactionMajorState.Done, - }; - case PeerPushDebitStatus.Failed: - return { - major: TransactionMajorState.Failed, - }; - case PeerPushDebitStatus.Expired: - return { - major: TransactionMajorState.Expired, - }; - } -} |