summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-09 16:23:26 +0100
committerFlorian Dold <florian@dold.me>2024-01-09 16:23:26 +0100
commitf8cde03f0cb6a7584fb92885f8979a01916a917d (patch)
tree6f120387f8f5297f436e2d2bd2d4d7b5c1814146
parentde39d432374a3ecd1bddd788b1ac1585461af8c1 (diff)
downloadwallet-core-f8cde03f0cb6a7584fb92885f8979a01916a917d.tar.gz
wallet-core-f8cde03f0cb6a7584fb92885f8979a01916a917d.tar.bz2
wallet-core-f8cde03f0cb6a7584fb92885f8979a01916a917d.zip
wallet-core: refactor peer-pull-debit and test aborting
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts145
-rw-r--r--packages/taler-util/src/transactions-types.ts4
-rw-r--r--packages/taler-wallet-core/src/db.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts19
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-common.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts499
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts16
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts26
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts12
-rw-r--r--packages/taler-wallet-core/src/util/query.ts98
-rw-r--r--packages/taler-wallet-core/tsconfig.json2
11 files changed, 510 insertions, 321 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
index 7ed716bc1..a71175407 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
@@ -25,10 +25,17 @@ import {
NotificationType,
TransactionMajorState,
TransactionMinorState,
+ TransactionType,
WalletNotification,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState } from "../harness/harness.js";
+import {
+ BankServiceHandle,
+ ExchangeService,
+ GlobalTestState,
+ WalletCli,
+ WalletClient,
+} from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV2,
createWalletDaemonWithClient,
@@ -65,6 +72,23 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
const wallet1 = w1.walletClient;
const wallet2 = w2.walletClient;
+ await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2);
+
+ console.log(`w1 notifications: ${j2s(allW1Notifications)}`);
+
+ // Check that we don't have an excessive number of notifications.
+ t.assertTrue(allW1Notifications.length <= 60);
+
+ await checkAbortedPeerPull(t, bank, exchange, wallet1, wallet2);
+}
+
+async function checkNormalPeerPull(
+ t: GlobalTestState,
+ bank: BankServiceHandle,
+ exchange: ExchangeService,
+ wallet1: WalletClient,
+ wallet2: WalletClient,
+): Promise<void> {
const withdrawRes = await withdrawViaBankV2(t, {
walletClient: wallet2,
bank,
@@ -94,7 +118,8 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
);
const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
- (x) => x.type === NotificationType.TransactionStateTransition &&
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId &&
x.newTxState.major === TransactionMajorState.Pending &&
x.newTxState.minor === TransactionMinorState.Ready,
@@ -102,23 +127,32 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
await peerPullCreditReadyCond;
+ const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp.transactionId,
+ });
+
+ t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!creditTx.talerUri);
+
const checkResp = await wallet2.client.call(
WalletApiOperation.PreparePeerPullDebit,
{
- talerUri: resp.talerUri,
+ talerUri: creditTx.talerUri,
},
);
console.log(`checkResp: ${j2s(checkResp)}`);
const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
- (x) => x.type === NotificationType.TransactionStateTransition &&
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId &&
x.newTxState.major === TransactionMajorState.Done,
);
const peerPullDebitDoneCond = wallet2.waitForNotificationCond(
- (x) => x.type === NotificationType.TransactionStateTransition &&
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
x.transactionId === checkResp.transactionId &&
x.newTxState.major === TransactionMajorState.Done,
);
@@ -142,11 +176,106 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
console.log(`txn1: ${j2s(txn1)}`);
console.log(`txn2: ${j2s(txn2)}`);
+}
- console.log(`w1 notifications: ${j2s(allW1Notifications)}`);
+async function checkAbortedPeerPull(
+ t: GlobalTestState,
+ bank: BankServiceHandle,
+ exchange: ExchangeService,
+ wallet1: WalletClient,
+ wallet2: WalletClient,
+): Promise<void> {
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
- // Check that we don't have an excessive number of notifications.
- t.assertTrue(allW1Notifications.length <= 60);
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration: purseExpiration,
+ },
+ },
+ );
+
+ const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPullCreditReadyCond;
+
+ const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp.transactionId,
+ });
+
+ t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!creditTx.talerUri);
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: creditTx.talerUri,
+ },
+ );
+
+ console.log(`checkResp: ${j2s(checkResp)}`);
+
+ const peerPullCreditAbortedCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Aborted,
+ );
+
+ const peerPullDebitAbortedCond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === checkResp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Aborted,
+ );
+
+ await wallet1.call(WalletApiOperation.AbortTransaction, {
+ transactionId: resp.transactionId,
+ });
+
+ await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ peerPullDebitId: checkResp.peerPullDebitId,
+ });
+
+ await peerPullCreditAbortedCond;
+ await peerPullDebitAbortedCond;
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
}
runPeerToPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index 740478fb0..17b56d13b 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -369,8 +369,10 @@ export interface TransactionPeerPullCredit extends TransactionCommon {
/**
* URI to send to the other party.
+ *
+ * Only available in the right state.
*/
- talerUri: string;
+ talerUri: string | undefined;
}
/**
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 76bb2e393..549bc7517 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -63,7 +63,9 @@ import { DbRetryInfo, TaskIdentifiers } from "./operations/common.js";
import {
DbAccess,
DbReadOnlyTransaction,
+ DbReadOnlyTransactionArr,
DbReadWriteTransaction,
+ DbReadWriteTransactionArr,
GetReadWriteAccess,
IndexDescriptor,
StoreDescriptor,
@@ -2639,6 +2641,8 @@ export const WalletStoresV1 = {
),
};
+type WalletStoreNames = StoreNames<typeof WalletStoresV1>;
+
export type WalletDbReadOnlyTransaction<
Stores extends StoreNames<typeof WalletStoresV1> & string,
> = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>;
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index d8fb82be1..1103b7255 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -1075,3 +1075,22 @@ export namespace TaskIdentifiers {
return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushCreditId}` as TaskId;
}
}
+
+/**
+ * Result of a transaction transition.
+ */
+export enum TransitionResult {
+ Transition = 1,
+ Stay = 2,
+}
+
+/**
+ * Transaction context.
+ *
+ * FIXME: Should eventually be implemented by all transactions.
+ */
+export interface TransactionContext {
+ abortTransaction(): Promise<void>;
+ resumeTransaction(): Promise<void>;
+ failTransaction(): Promise<void>;
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 1a5dc6e89..88eedb530 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -44,11 +44,7 @@ import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
const logger = new Logger("operations/peer-to-peer.ts");
/**
- * Get information about the coin selected for signatures
- *
- * @param ws
- * @param csel
- * @returns
+ * Get information about the coin selected for signatures.
*/
export async function queryCoinInfosForSelection(
ws: InternalWalletState,
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
index 72e9e2e4a..9bbe2c875 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022-2023 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -14,6 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * @fileoverview
+ * Implementation of the peer-pull-debit transaction, i.e.
+ * paying for an invoice the wallet received from another wallet.
+ */
+
+/**
+ * Imports.
+ */
import {
AcceptPeerPullPaymentResponse,
Amounts,
@@ -53,19 +62,25 @@ import {
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import {
+ DbReadWriteTransactionArr,
InternalWalletState,
PeerPullDebitRecordStatus,
PeerPullPaymentIncomingRecord,
PendingTaskType,
RefreshOperationStatus,
+ StoreNames,
+ WalletStoresV1,
createRefreshGroup,
timestampPreciseToDb,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
import { checkLogicInvariant } from "../util/invariants.js";
import {
TaskRunResult,
TaskRunResultType,
+ TransactionContext,
+ TransitionResult,
constructTaskIdentifier,
spendCoins,
} from "./common.js";
@@ -80,19 +95,181 @@ import {
parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
-import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-pull-debit.ts");
+/**
+ * Common context for a peer-pull-debit transaction.
+ */
+export class PeerPullDebitTransactionContext implements TransactionContext {
+ ws: InternalWalletState;
+ transactionId: string;
+ taskId: string;
+ peerPullDebitId: string;
+
+ constructor(ws: InternalWalletState, peerPullDebitId: string) {
+ this.ws = ws;
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullDebit,
+ peerPullDebitId,
+ });
+ this.peerPullDebitId = peerPullDebitId;
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const ctx = this;
+ stopLongpolling(ctx.ws, ctx.taskId);
+ await ctx.transition(async (pi) => {
+ switch (pi.status) {
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+ return TransitionResult.Transition;
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+ return TransitionResult.Transition;
+ case PeerPullDebitRecordStatus.Aborted:
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ case PeerPullDebitRecordStatus.Failed:
+ case PeerPullDebitRecordStatus.DialogProposed:
+ case PeerPullDebitRecordStatus.Done:
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ return TransitionResult.Stay;
+ }
+ });
+ }
+
+ async failTransaction(): Promise<void> {
+ const ctx = this;
+ stopLongpolling(ctx.ws, ctx.taskId);
+ await ctx.transition(async (pi) => {
+ switch (pi.status) {
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ // FIXME: Should we also abort the corresponding refresh session?!
+ pi.status = PeerPullDebitRecordStatus.Failed;
+ return TransitionResult.Transition;
+ default:
+ return TransitionResult.Stay;
+ }
+ });
+ }
+
+ async abortTransaction(): Promise<void> {
+ const ctx = this;
+ await ctx.transitionExtra(
+ {
+ extraStores: [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "coins",
+ "coinAvailability",
+ ],
+ },
+ async (pi, tx) => {
+ switch (pi.status) {
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ break;
+ default:
+ return TransitionResult.Stay;
+ }
+ const currency = Amounts.currencyOf(pi.totalCostEstimated);
+ const coinPubs: CoinRefreshRequest[] = [];
+
+ if (!pi.coinSel) {
+ throw Error("invalid db state");
+ }
+
+ for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: pi.coinSel.contributions[i],
+ coinPub: pi.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ ctx.ws,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPullDebit,
+ );
+
+ pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+ pi.abortRefreshGroupId = refresh.refreshGroupId;
+ return TransitionResult.Transition;
+ },
+ );
+ }
+
+ async transition(
+ f: (rec: PeerPullPaymentIncomingRecord) => Promise<TransitionResult>,
+ ): Promise<void> {
+ return this.transitionExtra(
+ {
+ extraStores: [],
+ },
+ f,
+ );
+ }
+
+ async transitionExtra<
+ StoreNameArray extends Array<StoreNames<typeof WalletStoresV1>> = [],
+ >(
+ opts: { extraStores: StoreNameArray },
+ f: (
+ rec: PeerPullPaymentIncomingRecord,
+ tx: DbReadWriteTransactionArr<
+ typeof WalletStoresV1,
+ ["peerPullDebit", ...StoreNameArray]
+ >,
+ ) => Promise<TransitionResult>,
+ ): Promise<void> {
+ const ws = this.ws;
+ const extraStores = opts.extraStores ?? [];
+ const transitionInfo = await ws.db.runReadWriteTx(
+ ["peerPullDebit", ...extraStores],
+ async (tx) => {
+ const pi = await tx.peerPullDebit.get(this.peerPullDebitId);
+ if (!pi) {
+ throw Error("peer pull payment not found anymore");
+ }
+ const oldTxState = computePeerPullDebitTransactionState(pi);
+ const res = await f(pi, tx);
+ switch (res) {
+ case TransitionResult.Transition: {
+ await tx.peerPullDebit.put(pi);
+ const newTxState = computePeerPullDebitTransactionState(pi);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ default:
+ return undefined;
+ }
+ },
+ );
+ notifyTransition(ws, this.transactionId, transitionInfo);
+ }
+}
+
async function handlePurseCreationConflict(
- ws: InternalWalletState,
+ ctx: PeerPullDebitTransactionContext,
peerPullInc: PeerPullPaymentIncomingRecord,
resp: HttpResponse,
): Promise<TaskRunResult> {
- const pursePub = peerPullInc.pursePub;
+ const ws = ctx.ws;
const errResp = await readTalerErrorResponse(resp);
if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
- await failPeerPullDebitTransaction(ws, pursePub);
+ await ctx.failTransaction();
return TaskRunResult.finished();
}
@@ -139,29 +316,27 @@ async function handlePurseCreationConflict(
coinSelRes.result.coins,
);
- await ws.db
- .mktx((x) => [x.peerPullDebit])
- .runReadWrite(async (tx) => {
- const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
- if (!myPpi) {
- return;
- }
- switch (myPpi.status) {
- case PeerPullDebitRecordStatus.PendingDeposit:
- case PeerPullDebitRecordStatus.SuspendedDeposit: {
- const sel = coinSelRes.result;
- myPpi.coinSel = {
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) => x.contribution),
- totalCost: Amounts.stringify(totalAmount),
- };
- break;
- }
- default:
- return;
+ await ws.db.runReadWriteTx(["peerPullDebit"], async (tx) => {
+ const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
+ if (!myPpi) {
+ return;
+ }
+ switch (myPpi.status) {
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ case PeerPullDebitRecordStatus.SuspendedDeposit: {
+ const sel = coinSelRes.result;
+ myPpi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ break;
}
- await tx.peerPullDebit.put(myPpi);
- });
+ default:
+ return;
+ }
+ await tx.peerPullDebit.put(myPpi);
+ });
return TaskRunResult.finished();
}
@@ -169,7 +344,6 @@ async function processPeerPullDebitPendingDeposit(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
): Promise<TaskRunResult> {
- const peerPullDebitId = peerPullInc.peerPullDebitId;
const pursePub = peerPullInc.pursePub;
const coinSel = peerPullInc.coinSel;
@@ -198,15 +372,16 @@ async function processPeerPullDebitPendingDeposit(
logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
}
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- });
-
const httpResp = await ws.http.fetch(purseDepositUrl.href, {
method: "POST",
body: depositPayload,
});
+
+ const ctx = new PeerPullDebitTransactionContext(
+ ws,
+ peerPullInc.peerPullDebitId,
+ );
+
switch (httpResp.status) {
case HttpStatusCode.Ok: {
const resp = await readSuccessResponseJsonOrThrow(
@@ -215,77 +390,21 @@ async function processPeerPullDebitPendingDeposit(
);
logger.trace(`purse deposit response: ${j2s(resp)}`);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPullDebit])
- .runReadWrite(async (tx) => {
- const pi = await tx.peerPullDebit.get(peerPullDebitId);
- if (!pi) {
- throw Error("peer pull payment not found anymore");
- }
- if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
- return;
- }
- const oldTxState = computePeerPullDebitTransactionState(pi);
- pi.status = PeerPullDebitRecordStatus.Done;
- const newTxState = computePeerPullDebitTransactionState(pi);
- await tx.peerPullDebit.put(pi);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- break;
+ await ctx.transition(async (r) => {
+ if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return TransitionResult.Stay;
+ }
+ r.status = PeerPullDebitRecordStatus.Done;
+ return TransitionResult.Transition;
+ });
+ return TaskRunResult.finished();
}
case HttpStatusCode.Gone: {
- const transitionInfo = await ws.db
- .mktx((x) => [
- x.peerPullDebit,
- x.refreshGroups,
- x.denominations,
- x.coinAvailability,
- x.coins,
- ])
- .runReadWrite(async (tx) => {
- const pi = await tx.peerPullDebit.get(peerPullDebitId);
- if (!pi) {
- throw Error("peer pull payment not found anymore");
- }
- if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
- return;
- }
- const oldTxState = computePeerPullDebitTransactionState(pi);
-
- const currency = Amounts.currencyOf(pi.totalCostEstimated);
- const coinPubs: CoinRefreshRequest[] = [];
-
- if (!pi.coinSel) {
- throw Error("invalid db state");
- }
-
- for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
- coinPubs.push({
- amount: pi.coinSel.contributions[i],
- coinPub: pi.coinSel.coinPubs[i],
- });
- }
-
- const refresh = await createRefreshGroup(
- ws,
- tx,
- currency,
- coinPubs,
- RefreshReason.AbortPeerPullDebit,
- );
-
- pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
- pi.abortRefreshGroupId = refresh.refreshGroupId;
- const newTxState = computePeerPullDebitTransactionState(pi);
- await tx.peerPullDebit.put(pi);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- break;
+ await ctx.abortTransaction();
+ return TaskRunResult.finished();
}
case HttpStatusCode.Conflict: {
- return handlePurseCreationConflict(ws, peerPullInc, httpResp);
+ return handlePurseCreationConflict(ctx, peerPullInc, httpResp);
}
default: {
const errResp = await readTalerErrorResponse(httpResp);
@@ -295,7 +414,6 @@ async function processPeerPullDebitPendingDeposit(
};
}
}
- return TaskRunResult.finished();
}
async function processPeerPullDebitAbortingRefresh(
@@ -624,6 +742,9 @@ export async function preparePeerPullDebit(
};
}
+/**
+ * FIXME: This belongs in the transaction context!
+ */
export async function suspendPeerPullDebitTransaction(
ws: InternalWalletState,
peerPullDebitId: string,
@@ -683,182 +804,6 @@ export async function suspendPeerPullDebitTransaction(
notifyTransition(ws, transactionId, transitionInfo);
}
-export async function abortPeerPullDebitTransaction(
- ws: InternalWalletState,
- peerPullDebitId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPullDebit,
- peerPullDebitId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPullDebit])
- .runReadWrite(async (tx) => {
- const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
- if (!pullDebitRec) {
- logger.warn(`peer pull debit ${peerPullDebitId} not found`);
- return;
- }
- let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
- switch (pullDebitRec.status) {
- case PeerPullDebitRecordStatus.DialogProposed:
- newStatus = PeerPullDebitRecordStatus.Aborted;
- break;
- case PeerPullDebitRecordStatus.Done:
- 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.peerPullDebit.put(pullDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failPeerPullDebitTransaction(
- ws: InternalWalletState,
- peerPullDebitId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPullDebit,
- peerPullDebitId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPullDebit])
- .runReadWrite(async (tx) => {
- const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
- if (!pullDebitRec) {
- logger.warn(`peer pull debit ${peerPullDebitId} not found`);
- return;
- }
- let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
- switch (pullDebitRec.status) {
- case PeerPullDebitRecordStatus.DialogProposed:
- newStatus = PeerPullDebitRecordStatus.Aborted;
- break;
- case PeerPullDebitRecordStatus.Done:
- 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.peerPullDebit.put(pullDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumePeerPullDebitTransaction(
- ws: InternalWalletState,
- peerPullDebitId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPullDebit,
- peerPullDebitId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPullDebit])
- .runReadWrite(async (tx) => {
- const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
- if (!pullDebitRec) {
- logger.warn(`peer pull debit ${peerPullDebitId} not found`);
- return;
- }
- let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
- switch (pullDebitRec.status) {
- case PeerPullDebitRecordStatus.DialogProposed:
- case PeerPullDebitRecordStatus.Done:
- 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.peerPullDebit.put(pullDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
export function computePeerPullDebitTransactionState(
pullDebitRecord: PeerPullPaymentIncomingRecord,
): TransactionState {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 17ac54cfb..a8bcb28d1 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -84,6 +84,7 @@ import {
RefreshSessionRecord,
timestampPreciseToDb,
timestampProtocolFromDb,
+ WalletDbReadWriteTransaction,
} from "../index.js";
import {
EXCHANGE_COINS_LOCK,
@@ -92,7 +93,11 @@ import {
import { assertUnreachable } from "../util/assertUnreachable.js";
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
+import {
+ DbReadWriteTransaction,
+ GetReadOnlyAccess,
+ GetReadWriteAccess,
+} from "../util/query.js";
import {
constructTaskIdentifier,
makeCoinAvailable,
@@ -1097,12 +1102,9 @@ async function applyRefresh(
*/
export async function createRefreshGroup(
ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- }>,
+ tx: WalletDbReadWriteTransaction<
+ "denominations" | "coins" | "refreshGroups" | "coinAvailability"
+ >,
currency: string,
oldCoinPubs: CoinRefreshRequest[],
reason: RefreshReason,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 3a219b39b..142eff7c1 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -114,11 +114,9 @@ import {
suspendPeerPullCreditTransaction,
} from "./pay-peer-pull-credit.js";
import {
- abortPeerPullDebitTransaction,
computePeerPullDebitTransactionActions,
computePeerPullDebitTransactionState,
- failPeerPullDebitTransaction,
- resumePeerPullDebitTransaction,
+ PeerPullDebitTransactionContext,
suspendPeerPullDebitTransaction,
} from "./pay-peer-pull-debit.js";
import {
@@ -1647,9 +1645,11 @@ export async function failTransaction(
case TransactionType.PeerPullCredit:
await failPeerPullCreditTransaction(ws, tx.pursePub);
return;
- case TransactionType.PeerPullDebit:
- await failPeerPullDebitTransaction(ws, tx.peerPullDebitId);
+ case TransactionType.PeerPullDebit: {
+ const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
+ await ctx.failTransaction();
return;
+ }
case TransactionType.PeerPushCredit:
await failPeerPushCreditTransaction(ws, tx.peerPushCreditId);
return;
@@ -1692,9 +1692,11 @@ export async function resumeTransaction(
case TransactionType.PeerPushDebit:
await resumePeerPushDebitTransaction(ws, tx.pursePub);
break;
- case TransactionType.PeerPullDebit:
- await resumePeerPullDebitTransaction(ws, tx.peerPullDebitId);
- break;
+ case TransactionType.PeerPullDebit: {
+ const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
+ await ctx.resumeTransaction();
+ return;
+ }
case TransactionType.PeerPushCredit:
await resumePeerPushCreditTransaction(ws, tx.peerPushCreditId);
break;
@@ -1936,9 +1938,11 @@ export async function abortTransaction(
case TransactionType.PeerPullCredit:
await abortPeerPullCreditTransaction(ws, txId.pursePub);
break;
- case TransactionType.PeerPullDebit:
- await abortPeerPullDebitTransaction(ws, txId.peerPullDebitId);
- break;
+ case TransactionType.PeerPullDebit: {
+ const ctx = new PeerPullDebitTransactionContext(ws, txId.peerPullDebitId);
+ await ctx.abortTransaction();
+ return;
+ }
case TransactionType.PeerPushCredit:
await abortPeerPushCreditTransaction(ws, txId.peerPushCreditId);
break;
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index 6070f4c78..9b29cee26 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -233,9 +233,9 @@ function tallyFees(
export type SelectPayCoinsResult =
| {
- type: "failure";
- insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
- }
+ type: "failure";
+ insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
+ }
| { type: "success"; coinSel: PayCoinSelection };
/**
@@ -889,9 +889,9 @@ export interface PeerCoinSelectionDetails {
export type SelectPeerCoinsResult =
| { type: "success"; result: PeerCoinSelectionDetails }
| {
- type: "failure";
- insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
- };
+ type: "failure";
+ insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+ };
export interface PeerCoinRepair {
exchangeBaseUrl: string;
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
index 309c17a43..5d563f620 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -454,8 +454,8 @@ type DerefKeyPath<T, P> = P extends `${infer PX extends keyof T &
KeyPathComponents}`
? T[PX]
: P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}`
- ? DerefKeyPath<T[P0], Rest>
- : unknown;
+ ? DerefKeyPath<T[P0], Rest>
+ : unknown;
/**
* Return a path if it is a valid dot-separate path to an object.
@@ -465,8 +465,8 @@ type ValidateKeyPath<T, P> = P extends `${infer PX extends keyof T &
KeyPathComponents}`
? PX
: P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}`
- ? `${P0}.${ValidateKeyPath<T[P0], Rest>}`
- : never;
+ ? `${P0}.${ValidateKeyPath<T[P0], Rest>}`
+ : never;
// function foo<T, P>(
// x: T,
@@ -545,11 +545,60 @@ type ReadWriteTransactionFunction<BoundStores, T> = (
rawTx: IDBTransaction,
) => Promise<T>;
+export type DbReadWriteTransactionArr<
+ StoreMap,
+ StoresArr extends Array<StoreNames<StoreMap>>,
+> = StoreMap extends {
+ [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>;
+}
+ ? {
+ [X in StoresArr[number] &
+ keyof StoreMap]: StoreMap[X] extends StoreWithIndexes<
+ infer _StoreName,
+ infer RecordType,
+ infer IndexMap
+ >
+ ? StoreReadWriteAccessor<RecordType, IndexMap>
+ : unknown;
+ }
+ : never;
+
+export type DbReadOnlyTransactionArr<
+ StoreMap,
+ StoresArr extends Array<StoreNames<StoreMap>>,
+> = StoreMap extends {
+ [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>;
+}
+ ? {
+ [X in StoresArr[number] &
+ keyof StoreMap]: StoreMap[X] extends StoreWithIndexes<
+ infer _StoreName,
+ infer RecordType,
+ infer IndexMap
+ >
+ ? StoreReadOnlyAccessor<RecordType, IndexMap>
+ : unknown;
+ }
+ : never;
+
export interface TransactionContext<BoundStores> {
runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
}
+/**
+ * Convert the type of an array to a union of the contents.
+ *
+ * Example:
+ * Input ["foo", "bar"]
+ * Output "foo" | "bar"
+ */
+export type UnionFromArray<Arr> = Arr extends {
+ [X in keyof Arr]: Arr[X] & string;
+}
+ ? Arr[keyof Arr & number]
+ : unknown;
+
function runTx<Arg, Res>(
tx: IDBTransaction,
arg: Arg,
@@ -743,7 +792,10 @@ type StoreNamesOf<X> = X extends { [x: number]: infer F }
* A store map is the metadata that describes the store.
*/
export class DbAccess<StoreMap> {
- constructor(private db: IDBDatabase, private stores: StoreMap) {}
+ constructor(
+ private db: IDBDatabase,
+ private stores: StoreMap,
+ ) {}
idbHandle(): IDBDatabase {
return this.db;
@@ -803,6 +855,42 @@ export class DbAccess<StoreMap> {
};
}
+ runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ storeNames: StoreNameArray,
+ txf: (
+ tx: DbReadWriteTransactionArr<StoreMap, StoreNameArray>,
+ ) => Promise<T>,
+ ): Promise<T> {
+ const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+ {};
+ const strStoreNames: string[] = [];
+ for (const sn of storeNames) {
+ const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+ strStoreNames.push(swi.storeName);
+ accessibleStores[swi.storeName] = swi;
+ }
+ const tx = this.db.transaction(strStoreNames, "readwrite");
+ const writeContext = makeWriteContext(tx, accessibleStores);
+ return runTx(tx, writeContext, txf);
+ }
+
+ runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ storeNames: StoreNameArray,
+ txf: (tx: DbReadOnlyTransactionArr<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T> {
+ const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+ {};
+ const strStoreNames: string[] = [];
+ for (const sn of storeNames) {
+ const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+ strStoreNames.push(swi.storeName);
+ accessibleStores[swi.storeName] = swi;
+ }
+ const tx = this.db.transaction(strStoreNames, "readwrite");
+ const readContext = makeReadContext(tx, accessibleStores);
+ return runTx(tx, readContext, txf);
+ }
+
/**
* Run a transaction with selected object stores.
*
diff --git a/packages/taler-wallet-core/tsconfig.json b/packages/taler-wallet-core/tsconfig.json
index 663a4dd98..7369e9783 100644
--- a/packages/taler-wallet-core/tsconfig.json
+++ b/packages/taler-wallet-core/tsconfig.json
@@ -15,7 +15,7 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
- "strictPropertyInitialization": false,
+ "strictPropertyInitialization": true,
"outDir": "lib",
"noImplicitAny": true,
"noImplicitThis": true,