summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
diff options
context:
space:
mode:
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.ts742
1 files changed, 742 insertions, 0 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
new file mode 100644
index 000000000..dead6313d
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
@@ -0,0 +1,742 @@
+/*
+ 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,
+ CheckPeerPushDebitRequest,
+ CheckPeerPushDebitResponse,
+ ContractTermsUtil,
+ HttpStatusCode,
+ InitiatePeerPushDebitRequest,
+ InitiatePeerPushDebitResponse,
+ Logger,
+ RefreshReason,
+ TalerError,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ TransactionAction,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ constructPayPushUri,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { InternalWalletState } from "../internal-wallet-state.js";
+import {
+ selectPeerCoins,
+ getTotalPeerPaymentCost,
+ codecForExchangePurseStatus,
+ queryCoinInfosForSelection,
+} from "./pay-peer-common.js";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ PeerPushPaymentInitiationRecord,
+ PeerPushPaymentInitiationStatus,
+} from "../index.js";
+import { PendingTaskType } from "../pending-types.js";
+import {
+ OperationAttemptResult,
+ OperationAttemptResultType,
+ constructTaskIdentifier,
+} from "../util/retries.js";
+import {
+ runLongpollAsync,
+ spendCoins,
+ runOperationWithErrorReporting,
+} from "./common.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+ stopLongpolling,
+} from "./transactions.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+
+const logger = new Logger("pay-peer-push-debit.ts");
+
+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 peer-push-debit 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,
+ }),
+ };
+}
+
+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 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 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,
+ };
+ }
+} \ No newline at end of file