summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-06-05 11:45:16 +0200
committerFlorian Dold <florian@dold.me>2023-06-05 11:45:16 +0200
commitfda5a0ed87a6473a6b34bd1ac07d5f1d45dfbc19 (patch)
treec8b7b09ca441d2a01e340dd3f569e075d3ef278e
parentf3d4ff4e3a44141ad387ef68a9083b01bf1c818a (diff)
downloadwallet-core-0.9.3-dev.14.tar.gz
wallet-core-0.9.3-dev.14.tar.bz2
wallet-core-0.9.3-dev.14.zip
wallet-core: restructure p2p implv0.9.3-dev.14
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-common.ts463
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts910
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts604
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts770
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts742
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer.ts3226
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts12
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts30
-rw-r--r--packages/taler-wallet-core/src/wallet.ts24
9 files changed, 3511 insertions, 3270 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
new file mode 100644
index 000000000..4b1dd31a5
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -0,0 +1,463 @@
+/*
+ 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 {
+ AgeCommitmentProof,
+ AmountJson,
+ AmountString,
+ Amounts,
+ Codec,
+ CoinPublicKeyString,
+ CoinStatus,
+ Logger,
+ PayPeerInsufficientBalanceDetails,
+ TalerProtocolTimestamp,
+ UnblindedSignature,
+ buildCodecForObject,
+ codecForAmountString,
+ codecForTimestamp,
+ codecOptional,
+ strcmp,
+} from "@gnu-taler/taler-util";
+import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
+import {
+ DenominationRecord,
+ PeerPushPaymentCoinSelection,
+ ReserveRecord,
+} from "../db.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
+import { checkDbInvariant } from "../util/invariants.js";
+import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
+import { getTotalRefreshCost } from "./refresh.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 interface PeerCoinSelectionRequest {
+ instructedAmount: AmountJson;
+
+ /**
+ * Instruct the coin selection to repair this coin
+ * selection instead of selecting completely new coins.
+ */
+ repair?: {
+ exchangeBaseUrl: string;
+ coinPubs: CoinPublicKeyString[];
+ contribs: AmountJson[];
+ };
+}
+
+export async function selectPeerCoins(
+ ws: InternalWalletState,
+ req: PeerCoinSelectionRequest,
+): Promise<SelectPeerCoinsResult> {
+ const instructedAmount = req.instructedAmount;
+ 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;
+ }
+ // FIXME: Can't we do this faster by using coinAvailability?
+ 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);
+
+ if (req.repair) {
+ for (let i = 0; i < req.repair.coinPubs.length; i++) {
+ const contrib = req.repair.contribs[i];
+ const coin = await tx.coins.get(req.repair.coinPubs[i]);
+ if (!coin) {
+ throw Error("repair not possible, coin not found");
+ }
+ const denom = await ws.getDenomInfo(
+ ws,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(!!denom);
+ resCoins.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: Amounts.stringify(contrib),
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
+ lastDepositFee = depositFee;
+ amountAcc = Amounts.add(
+ amountAcc,
+ Amounts.sub(contrib, depositFee).amount,
+ ).amount;
+ depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
+ }
+ }
+
+ 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;
+ });
+}
+
+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 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}`;
+}
+
+export 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;
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
new file mode 100644
index 000000000..b9c9728a1
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -0,0 +1,910 @@
+/*
+ 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 {
+ AbsoluteTime,
+ Amounts,
+ CancellationToken,
+ CheckPeerPullCreditRequest,
+ CheckPeerPullCreditResponse,
+ ContractTermsUtil,
+ ExchangeReservePurseRequest,
+ HttpStatusCode,
+ InitiatePeerPullCreditRequest,
+ InitiatePeerPullCreditResponse,
+ Logger,
+ TalerPreciseTimestamp,
+ TransactionAction,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ WalletAccountMergeFlags,
+ codecForAny,
+ codecForWalletKycUuid,
+ constructPayPullUri,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+} from "@gnu-taler/taler-util";
+import {
+ readSuccessResponseJsonOrErrorCode,
+ readSuccessResponseJsonOrThrow,
+ throwUnexpectedRequestError,
+} from "@gnu-taler/taler-util/http";
+import {
+ PeerPullPaymentInitiationRecord,
+ PeerPullPaymentInitiationStatus,
+ WithdrawalGroupStatus,
+ WithdrawalRecordType,
+ updateExchangeFromUrl,
+} from "../index.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
+import { PendingTaskType } from "../pending-types.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+import { checkDbInvariant } from "../util/invariants.js";
+import {
+ OperationAttemptResult,
+ OperationAttemptResultType,
+ constructTaskIdentifier,
+} from "../util/retries.js";
+import {
+ LongpollResult,
+ resetOperationTimeout,
+ runLongpollAsync,
+ runOperationWithErrorReporting,
+} from "./common.js";
+import {
+ codecForExchangePurseStatus,
+ getMergeReserveInfo,
+ talerPaytoFromExchangeReserve,
+} from "./pay-peer-common.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+ stopLongpolling,
+} from "./transactions.js";
+import {
+ checkWithdrawalKycStatus,
+ getExchangeWithdrawalInfo,
+ internalCreateWithdrawalGroup,
+ processWithdrawalGroup,
+} from "./withdraw.js";
+
+const logger = new Logger("pay-peer-pull-credit.ts");
+
+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,
+ };
+}
+
+/**
+ * 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,
+ };
+}
+
+/**
+ * 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;
+}
+
+/**
+ * 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 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 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];
+ }
+}
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];
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
new file mode 100644
index 000000000..69e0f3c27
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -0,0 +1,770 @@
+/*
+ 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 {
+ PreparePeerPushCredit,
+ PreparePeerPushCreditResponse,
+ parsePayPushUri,
+ codecForPeerContractTerms,
+ TransactionType,
+ encodeCrock,
+ eddsaGetPublic,
+ decodeCrock,
+ codecForExchangeGetContractResponse,
+ getRandomBytes,
+ ContractTermsUtil,
+ Amounts,
+ TalerPreciseTimestamp,
+ AcceptPeerPushPaymentResponse,
+ ConfirmPeerPushCreditRequest,
+ ExchangePurseMergeRequest,
+ HttpStatusCode,
+ PeerContractTerms,
+ TalerProtocolTimestamp,
+ WalletAccountMergeFlags,
+ codecForAny,
+ codecForWalletKycUuid,
+ j2s,
+ Logger,
+ ExchangePurseDeposits,
+ TransactionAction,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ InternalWalletState,
+ PeerPullDebitRecordStatus,
+ PeerPushPaymentIncomingRecord,
+ PeerPushPaymentIncomingStatus,
+ PendingTaskType,
+ WithdrawalGroupStatus,
+ WithdrawalRecordType,
+} from "../index.js";
+import { updateExchangeFromUrl } from "./exchanges.js";
+import {
+ codecForExchangePurseStatus,
+ getMergeReserveInfo,
+ queryCoinInfosForSelection,
+ talerPaytoFromExchangeReserve,
+} from "./pay-peer-common.js";
+import { constructTransactionIdentifier, notifyTransition, stopLongpolling } from "./transactions.js";
+import {
+ checkWithdrawalKycStatus,
+ getExchangeWithdrawalInfo,
+ internalCreateWithdrawalGroup,
+} from "./withdraw.js";
+import { checkDbInvariant } from "../util/invariants.js";
+import {
+ OperationAttemptResult,
+ OperationAttemptResultType,
+ constructTaskIdentifier,
+} from "../util/retries.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+
+const logger = new Logger("pay-peer-push-credit.ts");
+
+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 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 suspendPeerPushCreditTransaction(
+ ws: InternalWalletState,
+ peerPushPaymentIncomingId: string,
+) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushPaymentIncomingId,
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushPaymentIncomingId,
+ });
+ stopLongpolling(ws, taskId);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ const pushCreditRec = await tx.peerPushPaymentIncoming.get(
+ peerPushPaymentIncomingId,
+ );
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
+ return;
+ }
+ let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushPaymentIncomingStatus.DialogProposed:
+ case PeerPushPaymentIncomingStatus.Done:
+ case PeerPushPaymentIncomingStatus.SuspendedMerge:
+ case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
+ case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
+ break;
+ case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
+ newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPushPaymentIncomingStatus.PendingMerge:
+ newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge;
+ break;
+ case PeerPushPaymentIncomingStatus.PendingWithdrawing:
+ // FIXME: Suspend internal withdrawal transaction!
+ newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing;
+ break;
+ case PeerPushPaymentIncomingStatus.Aborted:
+ break;
+ case PeerPushPaymentIncomingStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushPaymentIncoming.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+}
+
+export async function abortPeerPushCreditTransaction(
+ ws: InternalWalletState,
+ peerPushPaymentIncomingId: string,
+) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushPaymentIncomingId,
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushPaymentIncomingId,
+ });
+ stopLongpolling(ws, taskId);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ const pushCreditRec = await tx.peerPushPaymentIncoming.get(
+ peerPushPaymentIncomingId,
+ );
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
+ return;
+ }
+ let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushPaymentIncomingStatus.DialogProposed:
+ newStatus = PeerPushPaymentIncomingStatus.Aborted;
+ break;
+ case PeerPushPaymentIncomingStatus.Done:
+ break;
+ case PeerPushPaymentIncomingStatus.SuspendedMerge:
+ case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
+ case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
+ newStatus = PeerPushPaymentIncomingStatus.Aborted;
+ break;
+ case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
+ newStatus = PeerPushPaymentIncomingStatus.Aborted;
+ break;
+ case PeerPushPaymentIncomingStatus.PendingMerge:
+ newStatus = PeerPushPaymentIncomingStatus.Aborted;
+ break;
+ case PeerPushPaymentIncomingStatus.PendingWithdrawing:
+ newStatus = PeerPushPaymentIncomingStatus.Aborted;
+ break;
+ case PeerPushPaymentIncomingStatus.Aborted:
+ break;
+ case PeerPushPaymentIncomingStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushPaymentIncoming.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+}
+
+export async function failPeerPushCreditTransaction(
+ ws: InternalWalletState,
+ peerPushPaymentIncomingId: string,
+) {
+ // We don't have any "aborting" states!
+ throw Error("can't run cancel-aborting on peer-push-credit transaction");
+}
+
+export async function resumePeerPushCreditTransaction(
+ ws: InternalWalletState,
+ peerPushPaymentIncomingId: string,
+) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushPaymentIncomingId,
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushPaymentIncomingId,
+ });
+ stopLongpolling(ws, taskId);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ const pushCreditRec = await tx.peerPushPaymentIncoming.get(
+ peerPushPaymentIncomingId,
+ );
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
+ return;
+ }
+ let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushPaymentIncomingStatus.DialogProposed:
+ case PeerPushPaymentIncomingStatus.Done:
+ case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
+ case PeerPushPaymentIncomingStatus.PendingMerge:
+ case PeerPushPaymentIncomingStatus.PendingWithdrawing:
+ case PeerPushPaymentIncomingStatus.SuspendedMerge:
+ newStatus = PeerPushPaymentIncomingStatus.PendingMerge;
+ break;
+ case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
+ newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired;
+ break;
+ case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
+ // FIXME: resume underlying "internal-withdrawal" transaction.
+ newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing;
+ break;
+ case PeerPushPaymentIncomingStatus.Aborted:
+ break;
+ case PeerPushPaymentIncomingStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushPaymentIncoming.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+}
+
+export function computePeerPushCreditTransactionState(
+ pushCreditRecord: PeerPushPaymentIncomingRecord,
+): TransactionState {
+ switch (pushCreditRecord.status) {
+ case PeerPushPaymentIncomingStatus.DialogProposed:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.Proposed,
+ };
+ case PeerPushPaymentIncomingStatus.PendingMerge:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Merge,
+ };
+ case PeerPushPaymentIncomingStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case PeerPushPaymentIncomingStatus.PendingWithdrawing:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Withdraw,
+ };
+ case PeerPushPaymentIncomingStatus.SuspendedMerge:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Merge,
+ };
+ case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.MergeKycRequired,
+ };
+ case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Withdraw,
+ };
+ case PeerPushPaymentIncomingStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PeerPushPaymentIncomingStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ default:
+ assertUnreachable(pushCreditRecord.status);
+ }
+}
+
+export function computePeerPushCreditTransactionActions(
+ pushCreditRecord: PeerPushPaymentIncomingRecord,
+): TransactionAction[] {
+ switch (pushCreditRecord.status) {
+ case PeerPushPaymentIncomingStatus.DialogProposed:
+ return [];
+ case PeerPushPaymentIncomingStatus.PendingMerge:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPushPaymentIncomingStatus.Done:
+ return [TransactionAction.Delete];
+ case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPushPaymentIncomingStatus.PendingWithdrawing:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case PeerPushPaymentIncomingStatus.SuspendedMerge:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPushPaymentIncomingStatus.Aborted:
+ return [TransactionAction.Delete];
+ case PeerPushPaymentIncomingStatus.Failed:
+ return [TransactionAction.Delete];
+ default:
+ assertUnreachable(pushCreditRecord.status);
+ }
+} \ No newline at end of file
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
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];
- }
-}
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index ef5aa907d..238a5dc66 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -50,14 +50,10 @@ import { getBalances } from "./balance.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { acceptWithdrawalFromUri } from "./withdraw.js";
import { updateExchangeFromUrl } from "./exchanges.js";
-import {
- confirmPeerPullDebit,
- confirmPeerPushCredit,
- initiatePeerPullPayment,
- initiatePeerPushDebit,
- preparePeerPullDebit,
- preparePeerPushCredit,
-} from "./pay-peer.js";
+import { initiatePeerPullPayment } from "./pay-peer-pull-credit.js";
+import { preparePeerPullDebit, confirmPeerPullDebit } from "./pay-peer-pull-debit.js";
+import { preparePeerPushCredit, confirmPeerPushCredit } from "./pay-peer-push-credit.js";
+import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
const logger = new Logger("operations/testing.ts");
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index a0da95799..1bd024d28 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -93,32 +93,6 @@ import {
computePayMerchantTransactionActions,
} from "./pay-merchant.js";
import {
- abortPeerPullCreditTransaction,
- abortPeerPullDebitTransaction,
- abortPeerPushCreditTransaction,
- abortPeerPushDebitTransaction,
- failPeerPullCreditTransaction,
- failPeerPullDebitTransaction,
- failPeerPushCreditTransaction,
- failPeerPushDebitTransaction,
- computePeerPullCreditTransactionState,
- computePeerPullDebitTransactionState,
- computePeerPushCreditTransactionState,
- computePeerPushDebitTransactionState,
- resumePeerPullCreditTransaction,
- resumePeerPullDebitTransaction,
- resumePeerPushCreditTransaction,
- resumePeerPushDebitTransaction,
- suspendPeerPullCreditTransaction,
- suspendPeerPullDebitTransaction,
- suspendPeerPushCreditTransaction,
- suspendPeerPushDebitTransaction,
- computePeerPushDebitTransactionActions,
- computePeerPullDebitTransactionActions,
- computePeerPullCreditTransactionActions,
- computePeerPushCreditTransactionActions,
-} from "./pay-peer.js";
-import {
abortRefreshGroup,
failRefreshGroup,
computeRefreshTransactionState,
@@ -143,6 +117,10 @@ import {
suspendWithdrawalTransaction,
computeWithdrawalTransactionActions,
} from "./withdraw.js";
+import { computePeerPullCreditTransactionState, computePeerPullCreditTransactionActions, suspendPeerPullCreditTransaction, failPeerPullCreditTransaction, resumePeerPullCreditTransaction, abortPeerPullCreditTransaction } from "./pay-peer-pull-credit.js";
+import { computePeerPullDebitTransactionState, computePeerPullDebitTransactionActions, suspendPeerPullDebitTransaction, failPeerPullDebitTransaction, resumePeerPullDebitTransaction, abortPeerPullDebitTransaction } from "./pay-peer-pull-debit.js";
+import { computePeerPushCreditTransactionState, computePeerPushCreditTransactionActions, suspendPeerPushCreditTransaction, failPeerPushCreditTransaction, resumePeerPushCreditTransaction, abortPeerPushCreditTransaction } from "./pay-peer-push-credit.js";
+import { computePeerPushDebitTransactionState, computePeerPushDebitTransactionActions, suspendPeerPushDebitTransaction, failPeerPushDebitTransaction, resumePeerPushDebitTransaction, abortPeerPushDebitTransaction } from "./pay-peer-push-debit.js";
const logger = new Logger("taler-wallet-core:transactions.ts");
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index df48c0e19..d0c34588b 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -63,8 +63,6 @@ import {
codecForAddKnownBankAccounts,
codecForAny,
codecForApplyDevExperiment,
- codecForApplyRefundFromPurchaseIdRequest,
- codecForApplyRefundRequest,
codecForCancelAbortingTransactionRequest,
codecForCheckPeerPullPaymentRequest,
codecForCheckPeerPushDebitRequest,
@@ -196,22 +194,29 @@ import {
getContractTermsDetails,
preparePayForUri,
processPurchase,
+ startQueryRefund,
startRefundQueryForUri,
} from "./operations/pay-merchant.js";
import {
checkPeerPullPaymentInitiation,
- checkPeerPushDebit,
- confirmPeerPullDebit,
- confirmPeerPushCredit,
initiatePeerPullPayment,
- initiatePeerPushDebit,
+ processPeerPullCredit,
+} from "./operations/pay-peer-pull-credit.js";
+import {
+ confirmPeerPullDebit,
preparePeerPullDebit,
+} from "./operations/pay-peer-pull-debit.js";
+import {
+ confirmPeerPushCredit,
preparePeerPushCredit,
- processPeerPullCredit,
processPeerPullDebit,
processPeerPushCredit,
+} from "./operations/pay-peer-push-credit.js";
+import {
+ checkPeerPushDebit,
+ initiatePeerPushDebit,
processPeerPushDebit,
-} from "./operations/pay-peer.js";
+} from "./operations/pay-peer-push-debit.js";
import { getPendingOperations } from "./operations/pending.js";
import {
createRecoupGroup,
@@ -232,8 +237,8 @@ import {
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
import {
abortTransaction,
- failTransaction,
deleteTransaction,
+ failTransaction,
getTransactionById,
getTransactions,
parseTransactionIdentifier,
@@ -280,7 +285,6 @@ import {
WalletCoreApiClient,
WalletCoreResponseType,
} from "./wallet-api-types.js";
-import { startQueryRefund } from "./operations/pay-merchant.js";
const logger = new Logger("wallet.ts");