summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-15 17:36:50 +0100
committerFlorian Dold <florian@dold.me>2024-01-15 18:43:20 +0100
commit8da08fe4205c1e03eec3d4925c598be0b6769ba5 (patch)
treee81d59fa7a1ef40687fc16199e4d44dd70e02b5c /packages/taler-wallet-core
parent68f3bcdc6cece62176849ab065e82630ebc4deae (diff)
downloadwallet-core-8da08fe4205c1e03eec3d4925c598be0b6769ba5.tar.gz
wallet-core-8da08fe4205c1e03eec3d4925c598be0b6769ba5.tar.bz2
wallet-core-8da08fe4205c1e03eec3d4925c598be0b6769ba5.zip
wallet-core: uniform transaction interface, cleanup
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r--packages/taler-wallet-core/src/db.ts1
-rw-r--r--packages/taler-wallet-core/src/internal-wallet-state.ts11
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts404
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts422
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts523
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts129
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts458
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts522
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts287
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts387
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts416
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts482
-rw-r--r--packages/taler-wallet-core/src/util/query.ts6
-rw-r--r--packages/taler-wallet-core/src/wallet.ts8
15 files changed, 1939 insertions, 2127 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 263de9d4c..84066aaf0 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -957,6 +957,7 @@ export enum RewardRecordStatus {
DialogAccept = 0x0101_0000,
Done = 0x0500_0000,
Aborted = 0x0500_0000,
+ Failed = 0x0501_000,
}
export enum RefreshCoinStatus {
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts
index 94f2367e1..8c49f8e5e 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -69,16 +69,6 @@ export interface MerchantInfo {
protocolVersionCurrent: number;
}
-/**
- * Interface for merchant-related operations.
- */
-export interface MerchantOperations {
- getMerchantInfo(
- ws: InternalWalletState,
- merchantBaseUrl: string,
- ): Promise<MerchantInfo>;
-}
-
export interface RefreshOperations {
createRefreshGroup(
ws: InternalWalletState,
@@ -154,7 +144,6 @@ export interface InternalWalletState {
merchantInfoCache: Record<string, MerchantInfo>;
recoupOps: RecoupOperations;
- merchantOps: MerchantOperations;
refreshOps: RefreshOperations;
isTaskLoopRunning: boolean;
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index 1103b7255..f34190cef 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -76,10 +76,7 @@ import { PendingTaskType, TaskId } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
-import {
- constructTransactionIdentifier,
- parseTransactionIdentifier,
-} from "./transactions.js";
+import { constructTransactionIdentifier } from "./transactions.js";
const logger = new Logger("operations/common.ts");
@@ -1086,11 +1083,12 @@ export enum TransitionResult {
/**
* Transaction context.
- *
- * FIXME: Should eventually be implemented by all transactions.
+ * Uniform interface to all transactions.
*/
export interface TransactionContext {
abortTransaction(): Promise<void>;
+ suspendTransaction(): Promise<void>;
resumeTransaction(): Promise<void>;
failTransaction(): Promise<void>;
+ deleteTransaction(): Promise<void>;
}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index f158d9cf9..62c1e406c 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -15,6 +15,10 @@
*/
/**
+ * Implementation of the deposit transaction.
+ */
+
+/**
* Imports.
*/
import {
@@ -84,6 +88,7 @@ import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import {
TaskRunResult,
TombstoneTag,
+ TransactionContext,
constructTaskIdentifier,
runLongpollAsync,
spendCoins,
@@ -106,6 +111,194 @@ import {
*/
const logger = new Logger("deposits.ts");
+export class DepositTransactionContext implements TransactionContext {
+ private transactionId: string;
+ private retryTag: string;
+ constructor(
+ public ws: InternalWalletState,
+ public depositGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const depositGroupId = this.depositGroupId;
+ const ws = this.ws;
+ // FIXME: We should check first if we are in a final state
+ // where deletion is allowed.
+ await ws.db
+ .mktx((x) => [x.depositGroups, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const tipRecord = await tx.depositGroups.get(depositGroupId);
+ if (tipRecord) {
+ await tx.depositGroups.delete(depositGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
+ });
+ }
+ });
+ return;
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, depositGroupId, transactionId, retryTag } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ let newOpStatus: DepositOperationStatus | undefined;
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.PendingDeposit:
+ newOpStatus = DepositOperationStatus.SuspendedDeposit;
+ break;
+ case DepositOperationStatus.PendingKyc:
+ newOpStatus = DepositOperationStatus.SuspendedKyc;
+ break;
+ case DepositOperationStatus.PendingTrack:
+ newOpStatus = DepositOperationStatus.SuspendedTrack;
+ break;
+ case DepositOperationStatus.Aborting:
+ newOpStatus = DepositOperationStatus.SuspendedAborting;
+ break;
+ }
+ if (!newOpStatus) {
+ return undefined;
+ }
+ dg.operationStatus = newOpStatus;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ });
+ stopLongpolling(ws, retryTag);
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, depositGroupId, transactionId, retryTag } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return undefined;
+ case DepositOperationStatus.PendingDeposit: {
+ dg.operationStatus = DepositOperationStatus.Aborting;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ case DepositOperationStatus.SuspendedDeposit:
+ // FIXME: Can we abort a suspended transaction?!
+ return undefined;
+ }
+ return undefined;
+ });
+ stopLongpolling(ws, retryTag);
+ // Need to process the operation again.
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, depositGroupId, transactionId, retryTag } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't resume deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ let newOpStatus: DepositOperationStatus | undefined;
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.SuspendedDeposit:
+ newOpStatus = DepositOperationStatus.PendingDeposit;
+ break;
+ case DepositOperationStatus.SuspendedAborting:
+ newOpStatus = DepositOperationStatus.Aborting;
+ break;
+ case DepositOperationStatus.SuspendedKyc:
+ newOpStatus = DepositOperationStatus.PendingKyc;
+ break;
+ case DepositOperationStatus.SuspendedTrack:
+ newOpStatus = DepositOperationStatus.PendingTrack;
+ break;
+ }
+ if (!newOpStatus) {
+ return undefined;
+ }
+ dg.operationStatus = newOpStatus;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, depositGroupId, transactionId, retryTag } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't cancel aborting deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.Aborting: {
+ dg.operationStatus = DepositOperationStatus.Failed;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ }
+ return undefined;
+ });
+ // FIXME: Also cancel ongoing work (via cancellation token, once implemented)
+ stopLongpolling(ws, retryTag);
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
/**
* Get the (DD37-style) transaction status based on the
* database record of a deposit group.
@@ -204,217 +397,6 @@ export function computeDepositTransactionActions(
}
/**
- * Put a deposit group in a suspended state.
- * While the deposit group is suspended, no network requests
- * will be made to advance the transaction status.
- */
-export async function suspendDepositGroup(
- ws: InternalWalletState,
- depositGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
- const retryTag = constructTaskIdentifier({
- tag: PendingTaskType.Deposit,
- depositGroupId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- logger.warn(
- `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
- );
- return undefined;
- }
- const oldState = computeDepositTransactionStatus(dg);
- let newOpStatus: DepositOperationStatus | undefined;
- switch (dg.operationStatus) {
- case DepositOperationStatus.PendingDeposit:
- newOpStatus = DepositOperationStatus.SuspendedDeposit;
- break;
- case DepositOperationStatus.PendingKyc:
- newOpStatus = DepositOperationStatus.SuspendedKyc;
- break;
- case DepositOperationStatus.PendingTrack:
- newOpStatus = DepositOperationStatus.SuspendedTrack;
- break;
- case DepositOperationStatus.Aborting:
- newOpStatus = DepositOperationStatus.SuspendedAborting;
- break;
- }
- if (!newOpStatus) {
- return undefined;
- }
- dg.operationStatus = newOpStatus;
- await tx.depositGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeDepositTransactionStatus(dg),
- };
- });
- stopLongpolling(ws, retryTag);
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumeDepositGroup(
- ws: InternalWalletState,
- depositGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- logger.warn(
- `can't resume deposit group, depositGroupId=${depositGroupId} not found`,
- );
- return;
- }
- const oldState = computeDepositTransactionStatus(dg);
- let newOpStatus: DepositOperationStatus | undefined;
- switch (dg.operationStatus) {
- case DepositOperationStatus.SuspendedDeposit:
- newOpStatus = DepositOperationStatus.PendingDeposit;
- break;
- case DepositOperationStatus.SuspendedAborting:
- newOpStatus = DepositOperationStatus.Aborting;
- break;
- case DepositOperationStatus.SuspendedKyc:
- newOpStatus = DepositOperationStatus.PendingKyc;
- break;
- case DepositOperationStatus.SuspendedTrack:
- newOpStatus = DepositOperationStatus.PendingTrack;
- break;
- }
- if (!newOpStatus) {
- return undefined;
- }
- dg.operationStatus = newOpStatus;
- await tx.depositGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeDepositTransactionStatus(dg),
- };
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function abortDepositGroup(
- ws: InternalWalletState,
- depositGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
- const retryTag = constructTaskIdentifier({
- tag: PendingTaskType.Deposit,
- depositGroupId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- logger.warn(
- `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
- );
- return undefined;
- }
- const oldState = computeDepositTransactionStatus(dg);
- switch (dg.operationStatus) {
- case DepositOperationStatus.Finished:
- return undefined;
- case DepositOperationStatus.PendingDeposit: {
- dg.operationStatus = DepositOperationStatus.Aborting;
- await tx.depositGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeDepositTransactionStatus(dg),
- };
- }
- case DepositOperationStatus.SuspendedDeposit:
- // FIXME: Can we abort a suspended transaction?!
- return undefined;
- }
- return undefined;
- });
- stopLongpolling(ws, retryTag);
- // Need to process the operation again.
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failDepositTransaction(
- ws: InternalWalletState,
- depositGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
- const retryTag = constructTaskIdentifier({
- tag: PendingTaskType.Deposit,
- depositGroupId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- logger.warn(
- `can't cancel aborting deposit group, depositGroupId=${depositGroupId} not found`,
- );
- return undefined;
- }
- const oldState = computeDepositTransactionStatus(dg);
- switch (dg.operationStatus) {
- case DepositOperationStatus.SuspendedAborting:
- case DepositOperationStatus.Aborting: {
- dg.operationStatus = DepositOperationStatus.Failed;
- await tx.depositGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeDepositTransactionStatus(dg),
- };
- }
- }
- return undefined;
- });
- // FIXME: Also cancel ongoing work (via cancellation token, once implemented)
- stopLongpolling(ws, retryTag);
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function deleteDepositGroup(
- ws: InternalWalletState,
- depositGroupId: string,
-) {
- // FIXME: We should check first if we are in a final state
- // where deletion is allowed.
- await ws.db
- .mktx((x) => [x.depositGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.depositGroups.get(depositGroupId);
- if (tipRecord) {
- await tx.depositGroups.delete(depositGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
- });
- }
- });
-}
-
-/**
* Check whether the refresh associated with the
* aborting deposit group is done.
*
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index a81311702..bc9e94a21 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -94,7 +94,6 @@ import {
} from "@gnu-taler/taler-util/http";
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
import {
- BackupProviderStateTag,
CoinRecord,
DenominationRecord,
PurchaseRecord,
@@ -130,6 +129,8 @@ import {
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
} from "./common.js";
import {
calculateRefreshOutput,
@@ -147,6 +148,224 @@ import {
*/
const logger = new Logger("pay-merchant.ts");
+export class PayMerchantTransactionContext implements TransactionContext {
+ private transactionId: string;
+ private retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public proposalId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Purchase,
+ proposalId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, proposalId } = this;
+ await ws.db
+ .mktx((x) => [x.purchases, x.tombstones])
+ .runReadWrite(async (tx) => {
+ let found = false;
+ const purchase = await tx.purchases.get(proposalId);
+ if (purchase) {
+ found = true;
+ await tx.purchases.delete(proposalId);
+ }
+ if (found) {
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePayment + ":" + proposalId,
+ });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, proposalId, transactionId } = this;
+ stopLongpolling(ws, this.retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newStatus = transitionSuspend[purchase.purchaseStatus];
+ if (!newStatus) {
+ return undefined;
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, proposalId, transactionId } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [
+ x.purchases,
+ x.refreshGroups,
+ x.denominations,
+ x.coinAvailability,
+ x.coins,
+ x.operationRetries,
+ ])
+ .runReadWrite(async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ const oldStatus = purchase.purchaseStatus;
+ if (purchase.timestampFirstSuccessfulPay) {
+ // No point in aborting it. We don't even report an error.
+ logger.warn(`tried to abort successful payment`);
+ return;
+ }
+ if (oldStatus === PurchaseStatus.PendingPaying) {
+ purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
+ }
+ await tx.purchases.put(purchase);
+ if (oldStatus === PurchaseStatus.PendingPaying) {
+ if (purchase.payInfo) {
+ const coinSel = purchase.payInfo.payCoinSelection;
+ const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost);
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (let i = 0; i < coinSel.coinPubs.length; i++) {
+ refreshCoins.push({
+ amount: coinSel.coinContributions[i],
+ coinPub: coinSel.coinPubs[i],
+ });
+ }
+ await createRefreshGroup(
+ ws,
+ tx,
+ currency,
+ refreshCoins,
+ RefreshReason.AbortPay,
+ );
+ }
+ }
+ await tx.operationRetries.delete(this.retryTag);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, proposalId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newStatus = transitionResume[purchase.purchaseStatus];
+ if (!newStatus) {
+ return undefined;
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, proposalId, transactionId } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [
+ x.purchases,
+ x.refreshGroups,
+ x.denominations,
+ x.coinAvailability,
+ x.coins,
+ x.operationRetries,
+ ])
+ .runReadWrite(async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newState: PurchaseStatus | undefined = undefined;
+ switch (purchase.purchaseStatus) {
+ case PurchaseStatus.AbortingWithRefund:
+ newState = PurchaseStatus.FailedAbort;
+ break;
+ }
+ if (newState) {
+ purchase.purchaseStatus = newState;
+ await tx.purchases.put(purchase);
+ }
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ ws.workAvailable.trigger();
+ }
+}
+
+export class RefundTransactionContext implements TransactionContext {
+ public transactionId: string;
+ constructor(
+ public ws: InternalWalletState,
+ public refundGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, refundGroupId, transactionId } = this;
+ await ws.db
+ .mktx((x) => [x.refundGroups, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const refundRecord = await tx.refundGroups.get(refundGroupId);
+ if (!refundRecord) {
+ return;
+ }
+ await tx.refundGroups.delete(refundGroupId);
+ await tx.tombstones.put({ id: transactionId });
+ // FIXME: Also tombstone the refund items, so that they won't reappear.
+ });
+ }
+
+ suspendTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ abortTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ resumeTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ failTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+}
+
/**
* Compute the total cost of a payment to the customer.
*
@@ -949,27 +1168,6 @@ async function handleInsufficientFunds(
});
}
-async function unblockBackup(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
- await tx.backupProviders.indexes.byPaymentProposalId
- .iter(proposalId)
- .forEachAsync(async (bp) => {
- bp.state = {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: timestampPreciseToDb(
- TalerPreciseTimestamp.now(),
- ),
- };
- tx.backupProviders.put(bp);
- });
- });
-}
-
// FIXME: Should probably not be exported in its current state
// FIXME: Should take a transaction ID instead of a proposal ID
// FIXME: Does way more than checking the payment
@@ -1606,7 +1804,7 @@ export async function processPurchase(
}
}
-export async function processPurchasePay(
+async function processPurchasePay(
ws: InternalWalletState,
proposalId: string,
options: unknown = {},
@@ -1772,7 +1970,6 @@ export async function processPurchasePay(
}
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp);
- await unblockBackup(ws, proposalId);
} else {
const payAgainUrl = new URL(
`orders/${download.contractData.orderId}/paid`,
@@ -1799,7 +1996,6 @@ export async function processPurchasePay(
);
}
await storePayReplaySuccess(ws, proposalId, sessionId);
- await unblockBackup(ws, proposalId);
}
return TaskRunResult.finished();
@@ -1837,115 +2033,6 @@ export async function refuseProposal(
notifyTransition(ws, transactionId, transitionInfo);
}
-export async function abortPayMerchant(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [
- x.purchases,
- x.refreshGroups,
- x.denominations,
- x.coinAvailability,
- x.coins,
- x.operationRetries,
- ])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- const oldStatus = purchase.purchaseStatus;
- if (purchase.timestampFirstSuccessfulPay) {
- // No point in aborting it. We don't even report an error.
- logger.warn(`tried to abort successful payment`);
- return;
- }
- if (oldStatus === PurchaseStatus.PendingPaying) {
- purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
- }
- await tx.purchases.put(purchase);
- if (oldStatus === PurchaseStatus.PendingPaying) {
- if (purchase.payInfo) {
- const coinSel = purchase.payInfo.payCoinSelection;
- const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost);
- const refreshCoins: CoinRefreshRequest[] = [];
- for (let i = 0; i < coinSel.coinPubs.length; i++) {
- refreshCoins.push({
- amount: coinSel.coinContributions[i],
- coinPub: coinSel.coinPubs[i],
- });
- }
- await createRefreshGroup(
- ws,
- tx,
- currency,
- refreshCoins,
- RefreshReason.AbortPay,
- );
- }
- }
- await tx.operationRetries.delete(opId);
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
-export async function failPaymentTransaction(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [
- x.purchases,
- x.refreshGroups,
- x.denominations,
- x.coinAvailability,
- x.coins,
- x.operationRetries,
- ])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- let newState: PurchaseStatus | undefined = undefined;
- switch (purchase.purchaseStatus) {
- case PurchaseStatus.AbortingWithRefund:
- newState = PurchaseStatus.FailedAbort;
- break;
- }
- if (newState) {
- purchase.purchaseStatus = newState;
- await tx.purchases.put(purchase);
- }
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
const transitionSuspend: {
[x in PurchaseStatus]?: {
next: PurchaseStatus | undefined;
@@ -1990,73 +2077,6 @@ const transitionResume: {
},
};
-export async function suspendPayMerchant(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- stopLongpolling(ws, opId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- let newStatus = transitionSuspend[purchase.purchaseStatus];
- if (!newStatus) {
- return undefined;
- }
- await tx.purchases.put(purchase);
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
-export async function resumePayMerchant(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- const opId = constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId,
- });
- stopLongpolling(ws, opId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- const oldTxState = computePayMerchantTransactionState(purchase);
- let newStatus = transitionResume[purchase.purchaseStatus];
- if (!newStatus) {
- return undefined;
- }
- await tx.purchases.put(purchase);
- const newTxState = computePayMerchantTransactionState(purchase);
- return { oldTxState, newTxState };
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
- ws.workAvailable.trigger();
-}
-
export function computePayMerchantTransactionState(
purchaseRecord: PurchaseRecord,
): TransactionState {
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
index a90eceed7..e655eba4b 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -69,6 +69,8 @@ import {
LongpollResult,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
@@ -88,6 +90,275 @@ import {
const logger = new Logger("pay-peer-pull-credit.ts");
+export class PeerPullCreditTransactionContext implements TransactionContext {
+ private transactionId: string;
+ private retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public pursePub: string,
+ ) {
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullCredit,
+ pursePub,
+ });
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, pursePub } = this;
+ await ws.db
+ .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const pullIni = await tx.peerPullCredit.get(pursePub);
+ if (!pullIni) {
+ return;
+ }
+ if (pullIni.withdrawalGroupId) {
+ const withdrawalGroupId = pullIni.withdrawalGroupId;
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (withdrawalGroupRecord) {
+ await tx.withdrawalGroups.delete(withdrawalGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+ });
+ }
+ }
+ await tx.peerPullCredit.delete(pursePub);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
+ });
+ });
+
+ return;
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, pursePub, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPullCredit])
+ .runReadWrite(async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
+ break;
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
+ break;
+ case PeerPullPaymentCreditStatus.PendingReady:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
+ break;
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ newStatus =
+ PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
+ break;
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, pursePub, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPullCredit])
+ .runReadWrite(async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ case PeerPullPaymentCreditStatus.PendingReady:
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ break;
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ newStatus = PeerPullPaymentCreditStatus.Failed;
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, pursePub, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPullCredit])
+ .runReadWrite(async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ case PeerPullPaymentCreditStatus.PendingReady:
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.Aborted:
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ newStatus = PeerPullPaymentCreditStatus.PendingReady;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, pursePub, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPullCredit])
+ .runReadWrite(async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ break;
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ throw Error("can't abort anymore");
+ case PeerPullPaymentCreditStatus.PendingReady:
+ newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ break;
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
async function queryPurseForPeerPullCredit(
ws: InternalWalletState,
pullIni: PeerPullCreditRecord,
@@ -849,258 +1120,6 @@ export async function initiatePeerPullPayment(
};
}
-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.peerPullCredit])
- .runReadWrite(async (tx) => {
- const pullCreditRec = await tx.peerPullCredit.get(pursePub);
- if (!pullCreditRec) {
- logger.warn(`peer pull credit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
- switch (pullCreditRec.status) {
- case PeerPullPaymentCreditStatus.PendingCreatePurse:
- newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
- break;
- case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
- break;
- case PeerPullPaymentCreditStatus.PendingWithdrawing:
- newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
- break;
- case PeerPullPaymentCreditStatus.PendingReady:
- newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
- break;
- case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- newStatus = PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
- break;
- case PeerPullPaymentCreditStatus.Done:
- case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
- case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
- case PeerPullPaymentCreditStatus.SuspendedReady:
- case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
- case PeerPullPaymentCreditStatus.Aborted:
- case PeerPullPaymentCreditStatus.Failed:
- case PeerPullPaymentCreditStatus.Expired:
- case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
- break;
- default:
- assertUnreachable(pullCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
- pullCreditRec.status = newStatus;
- const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
- await tx.peerPullCredit.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.peerPullCredit])
- .runReadWrite(async (tx) => {
- const pullCreditRec = await tx.peerPullCredit.get(pursePub);
- if (!pullCreditRec) {
- logger.warn(`peer pull credit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
- switch (pullCreditRec.status) {
- case PeerPullPaymentCreditStatus.PendingCreatePurse:
- case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
- break;
- case PeerPullPaymentCreditStatus.PendingWithdrawing:
- throw Error("can't abort anymore");
- case PeerPullPaymentCreditStatus.PendingReady:
- newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
- break;
- case PeerPullPaymentCreditStatus.Done:
- case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
- case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
- case PeerPullPaymentCreditStatus.SuspendedReady:
- case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
- case PeerPullPaymentCreditStatus.Aborted:
- case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- case PeerPullPaymentCreditStatus.Failed:
- case PeerPullPaymentCreditStatus.Expired:
- case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
- break;
- default:
- assertUnreachable(pullCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
- pullCreditRec.status = newStatus;
- const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
- await tx.peerPullCredit.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.peerPullCredit])
- .runReadWrite(async (tx) => {
- const pullCreditRec = await tx.peerPullCredit.get(pursePub);
- if (!pullCreditRec) {
- logger.warn(`peer pull credit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
- switch (pullCreditRec.status) {
- case PeerPullPaymentCreditStatus.PendingCreatePurse:
- case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- case PeerPullPaymentCreditStatus.PendingWithdrawing:
- case PeerPullPaymentCreditStatus.PendingReady:
- case PeerPullPaymentCreditStatus.Done:
- case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
- case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
- case PeerPullPaymentCreditStatus.SuspendedReady:
- case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
- case PeerPullPaymentCreditStatus.Aborted:
- case PeerPullPaymentCreditStatus.Failed:
- case PeerPullPaymentCreditStatus.Expired:
- break;
- case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
- newStatus = PeerPullPaymentCreditStatus.Failed;
- break;
- default:
- assertUnreachable(pullCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
- pullCreditRec.status = newStatus;
- const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
- await tx.peerPullCredit.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.peerPullCredit])
- .runReadWrite(async (tx) => {
- const pullCreditRec = await tx.peerPullCredit.get(pursePub);
- if (!pullCreditRec) {
- logger.warn(`peer pull credit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
- switch (pullCreditRec.status) {
- case PeerPullPaymentCreditStatus.PendingCreatePurse:
- case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
- case PeerPullPaymentCreditStatus.PendingWithdrawing:
- case PeerPullPaymentCreditStatus.PendingReady:
- case PeerPullPaymentCreditStatus.AbortingDeletePurse:
- case PeerPullPaymentCreditStatus.Done:
- case PeerPullPaymentCreditStatus.Failed:
- case PeerPullPaymentCreditStatus.Expired:
- case PeerPullPaymentCreditStatus.Aborted:
- break;
- case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
- newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
- break;
- case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
- newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
- break;
- case PeerPullPaymentCreditStatus.SuspendedReady:
- newStatus = PeerPullPaymentCreditStatus.PendingReady;
- break;
- case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
- newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
- break;
- case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
- newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
- break;
- default:
- assertUnreachable(pullCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
- pullCreditRec.status = newStatus;
- const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
- await tx.peerPullCredit.put(pullCreditRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
export function computePeerPullCreditTransactionState(
pullCreditRecord: PeerPullCreditRecord,
): TransactionState {
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 9bbe2c875..0f9f29fb5 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
@@ -120,6 +120,73 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
this.peerPullDebitId = peerPullDebitId;
}
+ async deleteTransaction(): Promise<void> {
+ const transactionId = this.transactionId;
+ const ws = this.ws;
+ const peerPullDebitId = this.peerPullDebitId;
+ await ws.db
+ .mktx((x) => [x.peerPullDebit, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const debit = await tx.peerPullDebit.get(peerPullDebitId);
+ if (debit) {
+ await tx.peerPullDebit.delete(peerPullDebitId);
+ await tx.tombstones.put({ id: transactionId });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const taskId = this.taskId;
+ const transactionId = this.transactionId;
+ const ws = this.ws;
+ const peerPullDebitId = this.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:
+ break;
+ case PeerPullDebitRecordStatus.Done:
+ 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.peerPullDebit.put(pullDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
async resumeTransaction(): Promise<void> {
const ctx = this;
stopLongpolling(ctx.ws, ctx.taskId);
@@ -742,68 +809,6 @@ export async function preparePeerPullDebit(
};
}
-/**
- * FIXME: This belongs in the transaction context!
- */
-export async function suspendPeerPullDebitTransaction(
- 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:
- break;
- case PeerPullDebitRecordStatus.Done:
- 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.peerPullDebit.put(pullDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
export function computePeerPullDebitTransactionState(
pullDebitRecord: PeerPullPaymentIncomingRecord,
): TransactionState {
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
index 36606e732..c8cfaac7d 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -66,6 +66,8 @@ import { checkDbInvariant } from "../util/invariants.js";
import {
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
@@ -90,6 +92,268 @@ import {
const logger = new Logger("pay-peer-push-credit.ts");
+export class PeerPushCreditTransactionContext implements TransactionContext {
+ private transactionId: string;
+ private retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public peerPushCreditId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushCreditId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, peerPushCreditId } = this;
+ await ws.db.runReadWriteTx(
+ ["withdrawalGroups", "peerPushCredit", "tombstones"],
+ async (tx) => {
+ const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushInc) {
+ return;
+ }
+ if (pushInc.withdrawalGroupId) {
+ const withdrawalGroupId = pushInc.withdrawalGroupId;
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (withdrawalGroupRecord) {
+ await tx.withdrawalGroups.delete(withdrawalGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+ });
+ }
+ }
+ await tx.peerPushCredit.delete(peerPushCreditId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
+ });
+ },
+ );
+ return;
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, peerPushCreditId, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db.runReadWriteTx(
+ ["peerPushCredit"],
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ break;
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPushCreditStatus.PendingMerge:
+ newStatus = PeerPushCreditStatus.SuspendedMerge;
+ break;
+ case PeerPushCreditStatus.PendingWithdrawing:
+ // FIXME: Suspend internal withdrawal transaction!
+ newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
+ break;
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, peerPushCreditId, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db.runReadWriteTx(
+ ["peerPushCredit"],
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.Done:
+ break;
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.PendingMerge:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.PendingWithdrawing:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, peerPushCreditId, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db.runReadWriteTx(
+ ["peerPushCredit"],
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingWithdrawing:
+ case PeerPushCreditStatus.SuspendedMerge:
+ newStatus = PeerPushCreditStatus.PendingMerge;
+ break;
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
+ break;
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ // FIXME: resume underlying "internal-withdrawal" transaction.
+ newStatus = PeerPushCreditStatus.PendingWithdrawing;
+ break;
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, peerPushCreditId, retryTag, transactionId } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db.runReadWriteTx(
+ ["peerPushCredit"],
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.Aborted:
+ case PeerPushCreditStatus.Failed:
+ // Already in a final state.
+ return;
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingWithdrawing:
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ newStatus = PeerPushCreditStatus.Failed;
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
export async function preparePeerPushCredit(
ws: InternalWalletState,
req: PreparePeerPushCreditRequest,
@@ -688,200 +952,6 @@ export async function confirmPeerPushCredit(
};
}
-export async function suspendPeerPushCreditTransaction(
- ws: InternalWalletState,
- peerPushCreditId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPushCredit,
- peerPushCreditId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPushCredit])
- .runReadWrite(async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushCreditRec) {
- logger.warn(`peer push credit ${peerPushCreditId} not found`);
- return;
- }
- let newStatus: PeerPushCreditStatus | undefined = undefined;
- switch (pushCreditRec.status) {
- case PeerPushCreditStatus.DialogProposed:
- case PeerPushCreditStatus.Done:
- case PeerPushCreditStatus.SuspendedMerge:
- case PeerPushCreditStatus.SuspendedMergeKycRequired:
- case PeerPushCreditStatus.SuspendedWithdrawing:
- break;
- case PeerPushCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
- break;
- case PeerPushCreditStatus.PendingMerge:
- newStatus = PeerPushCreditStatus.SuspendedMerge;
- break;
- case PeerPushCreditStatus.PendingWithdrawing:
- // FIXME: Suspend internal withdrawal transaction!
- newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
- break;
- case PeerPushCreditStatus.Aborted:
- break;
- case PeerPushCreditStatus.Failed:
- break;
- default:
- assertUnreachable(pushCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
- pushCreditRec.status = newStatus;
- const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
- await tx.peerPushCredit.put(pushCreditRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function abortPeerPushCreditTransaction(
- ws: InternalWalletState,
- peerPushCreditId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPushCredit,
- peerPushCreditId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPushCredit])
- .runReadWrite(async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushCreditRec) {
- logger.warn(`peer push credit ${peerPushCreditId} not found`);
- return;
- }
- let newStatus: PeerPushCreditStatus | undefined = undefined;
- switch (pushCreditRec.status) {
- case PeerPushCreditStatus.DialogProposed:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
- case PeerPushCreditStatus.Done:
- break;
- case PeerPushCreditStatus.SuspendedMerge:
- case PeerPushCreditStatus.SuspendedMergeKycRequired:
- case PeerPushCreditStatus.SuspendedWithdrawing:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
- case PeerPushCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
- case PeerPushCreditStatus.PendingMerge:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
- case PeerPushCreditStatus.PendingWithdrawing:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
- case PeerPushCreditStatus.Aborted:
- break;
- case PeerPushCreditStatus.Failed:
- break;
- default:
- assertUnreachable(pushCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
- pushCreditRec.status = newStatus;
- const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
- await tx.peerPushCredit.put(pushCreditRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failPeerPushCreditTransaction(
- ws: InternalWalletState,
- peerPushCreditId: 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,
- peerPushCreditId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPushCredit,
- peerPushCreditId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.peerPushCredit])
- .runReadWrite(async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushCreditRec) {
- logger.warn(`peer push credit ${peerPushCreditId} not found`);
- return;
- }
- let newStatus: PeerPushCreditStatus | undefined = undefined;
- switch (pushCreditRec.status) {
- case PeerPushCreditStatus.DialogProposed:
- case PeerPushCreditStatus.Done:
- case PeerPushCreditStatus.PendingMergeKycRequired:
- case PeerPushCreditStatus.PendingMerge:
- case PeerPushCreditStatus.PendingWithdrawing:
- case PeerPushCreditStatus.SuspendedMerge:
- newStatus = PeerPushCreditStatus.PendingMerge;
- break;
- case PeerPushCreditStatus.SuspendedMergeKycRequired:
- newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
- break;
- case PeerPushCreditStatus.SuspendedWithdrawing:
- // FIXME: resume underlying "internal-withdrawal" transaction.
- newStatus = PeerPushCreditStatus.PendingWithdrawing;
- break;
- case PeerPushCreditStatus.Aborted:
- break;
- case PeerPushCreditStatus.Failed:
- break;
- default:
- assertUnreachable(pushCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
- pushCreditRec.status = newStatus;
- const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
- await tx.peerPushCredit.put(pushCreditRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
export function computePeerPushCreditTransactionState(
pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionState {
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
index a11ffe774..4fd1ef3b2 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
@@ -63,6 +63,7 @@ import { checkLogicInvariant } from "../util/invariants.js";
import {
TaskRunResult,
TaskRunResultType,
+ TransactionContext,
constructTaskIdentifier,
runLongpollAsync,
spendCoins,
@@ -80,6 +81,257 @@ import {
const logger = new Logger("pay-peer-push-debit.ts");
+export class PeerPushDebitTransactionContext implements TransactionContext {
+ public transactionId: string;
+ public retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public pursePub: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushDebit,
+ pursePub,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, pursePub, transactionId } = this;
+ await ws.db
+ .mktx((x) => [x.peerPushDebit, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const debit = await tx.peerPushDebit.get(pursePub);
+ if (debit) {
+ await tx.peerPushDebit.delete(pursePub);
+ await tx.tombstones.put({ id: transactionId });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, pursePub, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushDebit])
+ .runReadWrite(async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ newStatus = PeerPushDebitStatus.SuspendedCreatePurse;
+ break;
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted;
+ break;
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired;
+ break;
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.PendingReady:
+ newStatus = PeerPushDebitStatus.SuspendedReady;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Failed:
+ case PeerPushDebitStatus.Expired:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, pursePub, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushDebit])
+ .runReadWrite(async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ case PeerPushDebitStatus.PendingCreatePurse:
+ // Network request might already be in-flight!
+ newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Expired:
+ case PeerPushDebitStatus.Failed:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, pursePub, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushDebit])
+ .runReadWrite(async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ newStatus = PeerPushDebitStatus.AbortingRefreshDeleted;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ newStatus = PeerPushDebitStatus.AbortingRefreshExpired;
+ break;
+ case PeerPushDebitStatus.SuspendedReady:
+ newStatus = PeerPushDebitStatus.PendingReady;
+ break;
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ newStatus = PeerPushDebitStatus.PendingCreatePurse;
+ break;
+ case PeerPushDebitStatus.PendingCreatePurse:
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Failed:
+ case PeerPushDebitStatus.Expired:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, pursePub, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.peerPushDebit])
+ .runReadWrite(async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ // FIXME: What to do about the refresh group?
+ newStatus = PeerPushDebitStatus.Failed;
+ break;
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ case PeerPushDebitStatus.PendingCreatePurse:
+ newStatus = PeerPushDebitStatus.Failed;
+ break;
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Failed:
+ case PeerPushDebitStatus.Expired:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
export async function checkPeerPushDebit(
ws: InternalWalletState,
req: CheckPeerPushDebitRequest,
@@ -118,8 +370,9 @@ async function handlePurseCreationConflict(
): Promise<TaskRunResult> {
const pursePub = peerPushInitiation.pursePub;
const errResp = await readTalerErrorResponse(resp);
+ const ctx = new PeerPushDebitTransactionContext(ws, pursePub);
if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
- await failPeerPushDebitTransaction(ws, pursePub);
+ await ctx.failTransaction();
return TaskRunResult.finished();
}
@@ -189,10 +442,8 @@ async function processPeerPushDebitCreateReserve(
const pursePub = peerPushInitiation.pursePub;
const purseExpiration = peerPushInitiation.purseExpiration;
const hContractTerms = peerPushInitiation.contractTermsHash;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: pursePub,
- });
+ const ctx = new PeerPushDebitTransactionContext(ws, pursePub);
+ const transactionId = ctx.transactionId;
logger.trace(`processing ${transactionId} pending(create-reserve)`);
@@ -277,7 +528,7 @@ async function processPeerPushDebitCreateReserve(
break;
case HttpStatusCode.Forbidden: {
// FIXME: Store this error!
- await failPeerPushDebitTransaction(ws, pursePub);
+ await ctx.failTransaction();
return TaskRunResult.finished();
}
case HttpStatusCode.Conflict: {
@@ -838,265 +1089,6 @@ export function computePeerPushDebitTransactionActions(
}
}
-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.peerPushDebit])
- .runReadWrite(async (tx) => {
- const pushDebitRec = await tx.peerPushDebit.get(pursePub);
- if (!pushDebitRec) {
- logger.warn(`peer push debit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPushDebitStatus | undefined = undefined;
- switch (pushDebitRec.status) {
- case PeerPushDebitStatus.PendingReady:
- case PeerPushDebitStatus.SuspendedReady:
- newStatus = PeerPushDebitStatus.AbortingDeletePurse;
- break;
- case PeerPushDebitStatus.SuspendedCreatePurse:
- case PeerPushDebitStatus.PendingCreatePurse:
- // Network request might already be in-flight!
- newStatus = PeerPushDebitStatus.AbortingDeletePurse;
- break;
- case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
- case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
- case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
- case PeerPushDebitStatus.AbortingRefreshDeleted:
- case PeerPushDebitStatus.AbortingRefreshExpired:
- case PeerPushDebitStatus.Done:
- case PeerPushDebitStatus.AbortingDeletePurse:
- case PeerPushDebitStatus.Aborted:
- case PeerPushDebitStatus.Expired:
- case PeerPushDebitStatus.Failed:
- // Do nothing
- break;
- default:
- assertUnreachable(pushDebitRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
- pushDebitRec.status = newStatus;
- const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
- await tx.peerPushDebit.put(pushDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- 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.peerPushDebit])
- .runReadWrite(async (tx) => {
- const pushDebitRec = await tx.peerPushDebit.get(pursePub);
- if (!pushDebitRec) {
- logger.warn(`peer push debit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPushDebitStatus | undefined = undefined;
- switch (pushDebitRec.status) {
- case PeerPushDebitStatus.AbortingRefreshDeleted:
- case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
- // FIXME: What to do about the refresh group?
- newStatus = PeerPushDebitStatus.Failed;
- break;
- case PeerPushDebitStatus.AbortingDeletePurse:
- case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
- case PeerPushDebitStatus.AbortingRefreshExpired:
- case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
- case PeerPushDebitStatus.PendingReady:
- case PeerPushDebitStatus.SuspendedReady:
- case PeerPushDebitStatus.SuspendedCreatePurse:
- case PeerPushDebitStatus.PendingCreatePurse:
- newStatus = PeerPushDebitStatus.Failed;
- break;
- case PeerPushDebitStatus.Done:
- case PeerPushDebitStatus.Aborted:
- case PeerPushDebitStatus.Failed:
- case PeerPushDebitStatus.Expired:
- // Do nothing
- break;
- default:
- assertUnreachable(pushDebitRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
- pushDebitRec.status = newStatus;
- const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
- await tx.peerPushDebit.put(pushDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- 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.peerPushDebit])
- .runReadWrite(async (tx) => {
- const pushDebitRec = await tx.peerPushDebit.get(pursePub);
- if (!pushDebitRec) {
- logger.warn(`peer push debit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPushDebitStatus | undefined = undefined;
- switch (pushDebitRec.status) {
- case PeerPushDebitStatus.PendingCreatePurse:
- newStatus = PeerPushDebitStatus.SuspendedCreatePurse;
- break;
- case PeerPushDebitStatus.AbortingRefreshDeleted:
- newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted;
- break;
- case PeerPushDebitStatus.AbortingRefreshExpired:
- newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired;
- break;
- case PeerPushDebitStatus.AbortingDeletePurse:
- newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse;
- break;
- case PeerPushDebitStatus.PendingReady:
- newStatus = PeerPushDebitStatus.SuspendedReady;
- break;
- case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
- case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
- case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
- case PeerPushDebitStatus.SuspendedReady:
- case PeerPushDebitStatus.SuspendedCreatePurse:
- case PeerPushDebitStatus.Done:
- case PeerPushDebitStatus.Aborted:
- case PeerPushDebitStatus.Failed:
- case PeerPushDebitStatus.Expired:
- // Do nothing
- break;
- default:
- assertUnreachable(pushDebitRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
- pushDebitRec.status = newStatus;
- const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
- await tx.peerPushDebit.put(pushDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- 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.peerPushDebit])
- .runReadWrite(async (tx) => {
- const pushDebitRec = await tx.peerPushDebit.get(pursePub);
- if (!pushDebitRec) {
- logger.warn(`peer push debit ${pursePub} not found`);
- return;
- }
- let newStatus: PeerPushDebitStatus | undefined = undefined;
- switch (pushDebitRec.status) {
- case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
- newStatus = PeerPushDebitStatus.AbortingDeletePurse;
- break;
- case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
- newStatus = PeerPushDebitStatus.AbortingRefreshDeleted;
- break;
- case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
- newStatus = PeerPushDebitStatus.AbortingRefreshExpired;
- break;
- case PeerPushDebitStatus.SuspendedReady:
- newStatus = PeerPushDebitStatus.PendingReady;
- break;
- case PeerPushDebitStatus.SuspendedCreatePurse:
- newStatus = PeerPushDebitStatus.PendingCreatePurse;
- break;
- case PeerPushDebitStatus.PendingCreatePurse:
- case PeerPushDebitStatus.AbortingRefreshDeleted:
- case PeerPushDebitStatus.AbortingRefreshExpired:
- case PeerPushDebitStatus.AbortingDeletePurse:
- case PeerPushDebitStatus.PendingReady:
- case PeerPushDebitStatus.Done:
- case PeerPushDebitStatus.Aborted:
- case PeerPushDebitStatus.Failed:
- case PeerPushDebitStatus.Expired:
- // Do nothing
- break;
- default:
- assertUnreachable(pushDebitRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
- pushDebitRec.status = newStatus;
- const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
- await tx.peerPushDebit.put(pushDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
export function computePeerPushDebitTransactionState(
ppiRecord: PeerPushDebitRecord,
): TransactionState {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 390433f66..fc2508cd3 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -98,6 +98,8 @@ import {
makeCoinsVisible,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
} from "./common.js";
import { fetchFreshExchange } from "./exchanges.js";
import {
@@ -107,6 +109,152 @@ import {
const logger = new Logger("refresh.ts");
+export class RefreshTransactionContext implements TransactionContext {
+ public transactionId: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public refreshGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const refreshGroupId = this.refreshGroupId;
+ const ws = this.ws;
+ await ws.db
+ .mktx((x) => [x.refreshGroups, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (rg) {
+ await tx.refreshGroups.delete(refreshGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
+ });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, refreshGroupId, transactionId } = this;
+ let res = await ws.db
+ .mktx((x) => [x.refreshGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.refreshGroups.get(refreshGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend refresh group, refreshGroupId=${refreshGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeRefreshTransactionState(dg);
+ switch (dg.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ return undefined;
+ case RefreshOperationStatus.Pending: {
+ dg.operationStatus = RefreshOperationStatus.Suspended;
+ await tx.refreshGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeRefreshTransactionState(dg),
+ };
+ }
+ case RefreshOperationStatus.Suspended:
+ return undefined;
+ }
+ return undefined;
+ });
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
+ }
+
+ async abortTransaction(): Promise<void> {
+ // Refresh transactions only support fail, not abort.
+ throw new Error("refresh transactions cannot be aborted");
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, refreshGroupId, transactionId } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.refreshGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.refreshGroups.get(refreshGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
+ );
+ return;
+ }
+ const oldState = computeRefreshTransactionState(dg);
+ switch (dg.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ return;
+ case RefreshOperationStatus.Pending: {
+ return;
+ }
+ case RefreshOperationStatus.Suspended:
+ dg.operationStatus = RefreshOperationStatus.Pending;
+ await tx.refreshGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeRefreshTransactionState(dg),
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, refreshGroupId, transactionId } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.refreshGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.refreshGroups.get(refreshGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
+ );
+ return;
+ }
+ const oldState = computeRefreshTransactionState(dg);
+ let newStatus: RefreshOperationStatus | undefined;
+ switch (dg.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ break;
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ newStatus = RefreshOperationStatus.Failed;
+ break;
+ case RefreshOperationStatus.Failed:
+ break;
+ default:
+ assertUnreachable(dg.operationStatus);
+ }
+ if (newStatus) {
+ dg.operationStatus = newStatus;
+ await tx.refreshGroups.put(dg);
+ }
+ return {
+ oldTxState: oldState,
+ newTxState: computeRefreshTransactionState(dg),
+ };
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
/**
* Get the amount that we lose when refreshing a coin of the given denomination
* with a certain amount left.
@@ -1256,9 +1404,6 @@ export async function autoRefresh(
`created refresh group for auto-refresh (${res.refreshGroupId})`,
);
}
- // logger.trace(
- // `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
- // );
logger.trace(
`next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
);
@@ -1308,142 +1453,6 @@ export function computeRefreshTransactionActions(
}
}
-export async function suspendRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId,
- });
- let res = await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.refreshGroups.get(refreshGroupId);
- if (!dg) {
- logger.warn(
- `can't suspend refresh group, refreshGroupId=${refreshGroupId} not found`,
- );
- return undefined;
- }
- const oldState = computeRefreshTransactionState(dg);
- switch (dg.operationStatus) {
- case RefreshOperationStatus.Finished:
- return undefined;
- case RefreshOperationStatus.Pending: {
- dg.operationStatus = RefreshOperationStatus.Suspended;
- await tx.refreshGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeRefreshTransactionState(dg),
- };
- }
- case RefreshOperationStatus.Suspended:
- return undefined;
- }
- return undefined;
- });
- if (res) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId,
- oldTxState: res.oldTxState,
- newTxState: res.newTxState,
- });
- }
-}
-
-export async function resumeRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.refreshGroups.get(refreshGroupId);
- if (!dg) {
- logger.warn(
- `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
- );
- return;
- }
- const oldState = computeRefreshTransactionState(dg);
- switch (dg.operationStatus) {
- case RefreshOperationStatus.Finished:
- return;
- case RefreshOperationStatus.Pending: {
- return;
- }
- case RefreshOperationStatus.Suspended:
- dg.operationStatus = RefreshOperationStatus.Pending;
- await tx.refreshGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeRefreshTransactionState(dg),
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
-): Promise<void> {
- throw Error("action cancel-aborting not allowed on refreshes");
-}
-
-export async function abortRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
-): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId,
- });
- const transitionInfo = await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.refreshGroups.get(refreshGroupId);
- if (!dg) {
- logger.warn(
- `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
- );
- return;
- }
- const oldState = computeRefreshTransactionState(dg);
- let newStatus: RefreshOperationStatus | undefined;
- switch (dg.operationStatus) {
- case RefreshOperationStatus.Finished:
- break;
- case RefreshOperationStatus.Pending:
- case RefreshOperationStatus.Suspended:
- newStatus = RefreshOperationStatus.Failed;
- break;
- case RefreshOperationStatus.Failed:
- break;
- default:
- assertUnreachable(dg.operationStatus);
- }
- if (newStatus) {
- dg.operationStatus = newStatus;
- await tx.refreshGroups.put(dg);
- }
- return {
- oldTxState: oldState,
- newTxState: computeRefreshTransactionState(dg),
- };
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
export async function forceRefresh(
ws: InternalWalletState,
req: ForceRefreshRequest,
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 79beb6432..62ac81d7f 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -68,6 +68,8 @@ import {
makeCoinsVisible,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
} from "./common.js";
import { fetchFreshExchange } from "./exchanges.js";
import {
@@ -86,6 +88,202 @@ import { assertUnreachable } from "../util/assertUnreachable.js";
const logger = new Logger("operations/tip.ts");
+export class RewardTransactionContext implements TransactionContext {
+ public transactionId: string;
+ public retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public walletRewardId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Reward,
+ walletRewardId,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.RewardPickup,
+ walletRewardId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, walletRewardId } = this;
+ await ws.db
+ .mktx((x) => [x.rewards, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const tipRecord = await tx.rewards.get(walletRewardId);
+ if (tipRecord) {
+ await tx.rewards.delete(walletRewardId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteReward + ":" + walletRewardId,
+ });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const tipRec = await tx.rewards.get(walletRewardId);
+ if (!tipRec) {
+ logger.warn(`transaction tip ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (tipRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.SuspendedPickup:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.PendingPickup:
+ newStatus = RewardRecordStatus.SuspendedPickup;
+ break;
+
+ default:
+ assertUnreachable(tipRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(tipRec);
+ tipRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(tipRec);
+ await tx.rewards.put(tipRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const tipRec = await tx.rewards.get(walletRewardId);
+ if (!tipRec) {
+ logger.warn(`transaction tip ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (tipRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.PendingPickup:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.SuspendedPickup:
+ newStatus = RewardRecordStatus.Aborted;
+ break;
+ default:
+ assertUnreachable(tipRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(tipRec);
+ tipRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(tipRec);
+ await tx.rewards.put(tipRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const rewardRec = await tx.rewards.get(walletRewardId);
+ if (!rewardRec) {
+ logger.warn(`transaction reward ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (rewardRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.PendingPickup:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.SuspendedPickup:
+ newStatus = RewardRecordStatus.PendingPickup;
+ break;
+ default:
+ assertUnreachable(rewardRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(rewardRec);
+ rewardRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(rewardRec);
+ await tx.rewards.put(rewardRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const tipRec = await tx.rewards.get(walletRewardId);
+ if (!tipRec) {
+ logger.warn(`transaction tip ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (tipRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.PendingPickup:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.SuspendedPickup:
+ newStatus = RewardRecordStatus.Failed;
+ break;
+ default:
+ assertUnreachable(tipRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(tipRec);
+ tipRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(tipRec);
+ await tx.rewards.put(tipRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
/**
* Get the (DD37-style) transaction status based on the
* database record of a reward.
@@ -117,6 +315,10 @@ export function computeRewardTransactionStatus(
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup,
};
+ case RewardRecordStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
default:
assertUnreachable(tipRecord.status);
}
@@ -128,6 +330,8 @@ export function computeTipTransactionActions(
switch (tipRecord.status) {
case RewardRecordStatus.Done:
return [TransactionAction.Delete];
+ case RewardRecordStatus.Failed:
+ return [TransactionAction.Delete];
case RewardRecordStatus.Aborted:
return [TransactionAction.Delete];
case RewardRecordStatus.PendingPickup:
@@ -141,7 +345,7 @@ export function computeTipTransactionActions(
}
}
-export async function prepareTip(
+export async function prepareReward(
ws: InternalWalletState,
talerTipUri: string,
): Promise<PrepareTipResult> {
@@ -166,33 +370,33 @@ export async function prepareTip(
);
logger.trace("checking tip status from", tipStatusUrl.href);
const merchantResp = await ws.http.fetch(tipStatusUrl.href);
- const tipPickupStatus = await readSuccessResponseJsonOrThrow(
+ const rewardPickupStatus = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForRewardPickupGetResponse(),
);
- logger.trace(`status ${j2s(tipPickupStatus)}`);
+ logger.trace(`status ${j2s(rewardPickupStatus)}`);
- const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount);
+ const amount = Amounts.parseOrThrow(rewardPickupStatus.reward_amount);
const currency = amount.currency;
logger.trace("new tip, creating tip record");
- await fetchFreshExchange(ws, tipPickupStatus.exchange_url);
+ await fetchFreshExchange(ws, rewardPickupStatus.exchange_url);
//FIXME: is this needed? withdrawDetails is not used
// * if the intention is to update the exchange information in the database
// maybe we can use another name. `get` seems like a pure-function
const withdrawDetails = await getExchangeWithdrawalInfo(
ws,
- tipPickupStatus.exchange_url,
+ rewardPickupStatus.exchange_url,
amount,
undefined,
);
- const walletTipId = encodeCrock(getRandomBytes(32));
- await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
+ const walletRewardId = encodeCrock(getRandomBytes(32));
+ await updateWithdrawalDenoms(ws, rewardPickupStatus.exchange_url);
const denoms = await getCandidateWithdrawalDenoms(
ws,
- tipPickupStatus.exchange_url,
+ rewardPickupStatus.exchange_url,
currency,
);
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
@@ -201,13 +405,13 @@ export async function prepareTip(
const denomSelUid = encodeCrock(getRandomBytes(32));
const newTipRecord: RewardRecord = {
- walletRewardId: walletTipId,
+ walletRewardId: walletRewardId,
acceptedTimestamp: undefined,
status: RewardRecordStatus.DialogAccept,
rewardAmountRaw: Amounts.stringify(amount),
- rewardExpiration: timestampProtocolToDb(tipPickupStatus.expiration),
- exchangeBaseUrl: tipPickupStatus.exchange_url,
- next_url: tipPickupStatus.next_url,
+ rewardExpiration: timestampProtocolToDb(rewardPickupStatus.expiration),
+ exchangeBaseUrl: rewardPickupStatus.exchange_url,
+ next_url: rewardPickupStatus.next_url,
merchantBaseUrl: res.merchantBaseUrl,
createdTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
merchantRewardId: res.merchantRewardId,
@@ -485,160 +689,3 @@ export async function acceptTip(
next_url: tipRecord.next_url,
};
}
-
-export async function suspendRewardTransaction(
- ws: InternalWalletState,
- walletRewardId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: walletRewardId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: walletRewardId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.rewards])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.rewards.get(walletRewardId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletRewardId} not found`);
- return;
- }
- let newStatus: RewardRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case RewardRecordStatus.Done:
- case RewardRecordStatus.SuspendedPickup:
- case RewardRecordStatus.Aborted:
- case RewardRecordStatus.DialogAccept:
- break;
- case RewardRecordStatus.PendingPickup:
- newStatus = RewardRecordStatus.SuspendedPickup;
- break;
-
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeRewardTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeRewardTransactionStatus(tipRec);
- await tx.rewards.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumeTipTransaction(
- ws: InternalWalletState,
- walletRewardId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: walletRewardId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: walletRewardId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.rewards])
- .runReadWrite(async (tx) => {
- const rewardRec = await tx.rewards.get(walletRewardId);
- if (!rewardRec) {
- logger.warn(`transaction reward ${walletRewardId} not found`);
- return;
- }
- let newStatus: RewardRecordStatus | undefined = undefined;
- switch (rewardRec.status) {
- case RewardRecordStatus.Done:
- case RewardRecordStatus.PendingPickup:
- case RewardRecordStatus.Aborted:
- case RewardRecordStatus.DialogAccept:
- break;
- case RewardRecordStatus.SuspendedPickup:
- newStatus = RewardRecordStatus.PendingPickup;
- break;
- default:
- assertUnreachable(rewardRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeRewardTransactionStatus(rewardRec);
- rewardRec.status = newStatus;
- const newTxState = computeRewardTransactionStatus(rewardRec);
- await tx.rewards.put(rewardRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failTipTransaction(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<void> {
- // We don't have an "aborting" state, so this should never happen!
- throw Error("can't run cance-aborting on tip transaction");
-}
-
-export async function abortTipTransaction(
- ws: InternalWalletState,
- walletRewardId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: walletRewardId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: walletRewardId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.rewards])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.rewards.get(walletRewardId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletRewardId} not found`);
- return;
- }
- let newStatus: RewardRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case RewardRecordStatus.Done:
- case RewardRecordStatus.Aborted:
- case RewardRecordStatus.PendingPickup:
- case RewardRecordStatus.DialogAccept:
- break;
- case RewardRecordStatus.SuspendedPickup:
- newStatus = RewardRecordStatus.Aborted;
- break;
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeRewardTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeRewardTransactionStatus(tipRec);
- await tx.rewards.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 142eff7c1..908aa540a 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -79,61 +79,45 @@ import {
constructTaskIdentifier,
resetPendingTaskTimeout,
TaskIdentifiers,
- TombstoneTag,
+ TransactionContext,
} from "./common.js";
import {
- abortDepositGroup,
computeDepositTransactionActions,
computeDepositTransactionStatus,
- deleteDepositGroup,
- failDepositTransaction,
- resumeDepositGroup,
- suspendDepositGroup,
+ DepositTransactionContext,
} from "./deposits.js";
import {
ExchangeWireDetails,
getExchangeWireDetailsInTx,
} from "./exchanges.js";
import {
- abortPayMerchant,
computePayMerchantTransactionActions,
computePayMerchantTransactionState,
computeRefundTransactionState,
expectProposalDownload,
extractContractData,
- failPaymentTransaction,
- resumePayMerchant,
- suspendPayMerchant,
+ PayMerchantTransactionContext,
+ RefundTransactionContext,
} from "./pay-merchant.js";
import {
- abortPeerPullCreditTransaction,
computePeerPullCreditTransactionActions,
computePeerPullCreditTransactionState,
- failPeerPullCreditTransaction,
- resumePeerPullCreditTransaction,
- suspendPeerPullCreditTransaction,
+ PeerPullCreditTransactionContext,
} from "./pay-peer-pull-credit.js";
import {
computePeerPullDebitTransactionActions,
computePeerPullDebitTransactionState,
PeerPullDebitTransactionContext,
- suspendPeerPullDebitTransaction,
} from "./pay-peer-pull-debit.js";
import {
- abortPeerPushCreditTransaction,
computePeerPushCreditTransactionActions,
computePeerPushCreditTransactionState,
- failPeerPushCreditTransaction,
- resumePeerPushCreditTransaction,
- suspendPeerPushCreditTransaction,
+ PeerPushCreditTransactionContext,
} from "./pay-peer-push-credit.js";
import {
- abortPeerPushDebitTransaction,
computePeerPushDebitTransactionActions,
computePeerPushDebitTransactionState,
- failPeerPushDebitTransaction,
- resumePeerPushDebitTransaction,
- suspendPeerPushDebitTransaction,
+ PeerPushDebitTransactionContext,
} from "./pay-peer-push-debit.js";
import {
iterRecordsForDeposit,
@@ -148,29 +132,20 @@ import {
iterRecordsForWithdrawal,
} from "./pending.js";
import {
- abortRefreshGroup,
computeRefreshTransactionActions,
computeRefreshTransactionState,
- failRefreshGroup,
- resumeRefreshGroup,
- suspendRefreshGroup,
+ RefreshTransactionContext,
} from "./refresh.js";
import {
- abortTipTransaction,
computeRewardTransactionStatus,
computeTipTransactionActions,
- failTipTransaction,
- resumeTipTransaction,
- suspendRewardTransaction,
+ RewardTransactionContext,
} from "./reward.js";
import {
- abortWithdrawalTransaction,
augmentPaytoUrisForWithdrawal,
computeWithdrawalTransactionActions,
computeWithdrawalTransactionStatus,
- failWithdrawalTransaction,
- resumeWithdrawalTransaction,
- suspendWithdrawalTransaction,
+ WithdrawTransactionContext,
} from "./withdraw.js";
const logger = new Logger("taler-wallet-core:transactions.ts");
@@ -1565,100 +1540,61 @@ export async function retryTransaction(
}
}
-/**
- * Suspends a pending transaction, stopping any associated network activities,
- * but with a chance of trying again at a later time. This could be useful if
- * a user needs to save battery power or bandwidth and an operation is expected
- * to take longer (such as a backup, recovery or very large withdrawal operation).
- */
-export async function suspendTransaction(
+async function getContextForTransaction(
ws: InternalWalletState,
transactionId: string,
-): Promise<void> {
+): Promise<TransactionContext> {
const tx = parseTransactionIdentifier(transactionId);
if (!tx) {
throw Error("invalid transaction ID");
}
switch (tx.tag) {
case TransactionType.Deposit:
- await suspendDepositGroup(ws, tx.depositGroupId);
- return;
+ return new DepositTransactionContext(ws, tx.depositGroupId);
case TransactionType.Refresh:
- await suspendRefreshGroup(ws, tx.refreshGroupId);
- return;
+ return new RefreshTransactionContext(ws, tx.refreshGroupId);
case TransactionType.InternalWithdrawal:
case TransactionType.Withdrawal:
- await suspendWithdrawalTransaction(ws, tx.withdrawalGroupId);
- return;
+ return new WithdrawTransactionContext(ws, tx.withdrawalGroupId);
case TransactionType.Payment:
- await suspendPayMerchant(ws, tx.proposalId);
- return;
+ return new PayMerchantTransactionContext(ws, tx.proposalId);
case TransactionType.PeerPullCredit:
- await suspendPeerPullCreditTransaction(ws, tx.pursePub);
- break;
+ return new PeerPullCreditTransactionContext(ws, tx.pursePub);
case TransactionType.PeerPushDebit:
- await suspendPeerPushDebitTransaction(ws, tx.pursePub);
- break;
+ return new PeerPushDebitTransactionContext(ws, tx.pursePub);
case TransactionType.PeerPullDebit:
- await suspendPeerPullDebitTransaction(ws, tx.peerPullDebitId);
- break;
+ return new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
case TransactionType.PeerPushCredit:
- await suspendPeerPushCreditTransaction(ws, tx.peerPushCreditId);
- break;
+ return new PeerPushCreditTransactionContext(ws, tx.peerPushCreditId);
case TransactionType.Refund:
- throw Error("refund transactions can't be suspended or resumed");
+ return new RefundTransactionContext(ws, tx.refundGroupId);
case TransactionType.Reward:
- await suspendRewardTransaction(ws, tx.walletRewardId);
- break;
+ return new RewardTransactionContext(ws, tx.walletRewardId);
default:
assertUnreachable(tx);
}
}
+/**
+ * Suspends a pending transaction, stopping any associated network activities,
+ * but with a chance of trying again at a later time. This could be useful if
+ * a user needs to save battery power or bandwidth and an operation is expected
+ * to take longer (such as a backup, recovery or very large withdrawal operation).
+ */
+export async function suspendTransaction(
+ ws: InternalWalletState,
+ transactionId: string,
+): Promise<void> {
+ const ctx = await getContextForTransaction(ws, transactionId);
+ await ctx.suspendTransaction();
+}
+
export async function failTransaction(
ws: InternalWalletState,
transactionId: string,
): Promise<void> {
- const tx = parseTransactionIdentifier(transactionId);
- if (!tx) {
- throw Error("invalid transaction ID");
- }
- switch (tx.tag) {
- case TransactionType.Deposit:
- await failDepositTransaction(ws, tx.depositGroupId);
- return;
- case TransactionType.InternalWithdrawal:
- case TransactionType.Withdrawal:
- await failWithdrawalTransaction(ws, tx.withdrawalGroupId);
- return;
- case TransactionType.Payment:
- await failPaymentTransaction(ws, tx.proposalId);
- return;
- case TransactionType.Refund:
- throw Error("can't do cancel-aborting on refund transaction");
- case TransactionType.Reward:
- await failTipTransaction(ws, tx.walletRewardId);
- return;
- case TransactionType.Refresh:
- await failRefreshGroup(ws, tx.refreshGroupId);
- return;
- case TransactionType.PeerPullCredit:
- await failPeerPullCreditTransaction(ws, tx.pursePub);
- return;
- case TransactionType.PeerPullDebit: {
- const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
- await ctx.failTransaction();
- return;
- }
- case TransactionType.PeerPushCredit:
- await failPeerPushCreditTransaction(ws, tx.peerPushCreditId);
- return;
- case TransactionType.PeerPushDebit:
- await failPeerPushDebitTransaction(ws, tx.pursePub);
- return;
- default:
- assertUnreachable(tx);
- }
+ const ctx = await getContextForTransaction(ws, transactionId);
+ await ctx.failTransaction();
}
/**
@@ -1668,44 +1604,8 @@ export async function resumeTransaction(
ws: InternalWalletState,
transactionId: string,
): Promise<void> {
- const tx = parseTransactionIdentifier(transactionId);
- if (!tx) {
- throw Error("invalid transaction ID");
- }
- switch (tx.tag) {
- case TransactionType.Deposit:
- await resumeDepositGroup(ws, tx.depositGroupId);
- return;
- case TransactionType.Refresh:
- await resumeRefreshGroup(ws, tx.refreshGroupId);
- return;
- case TransactionType.InternalWithdrawal:
- case TransactionType.Withdrawal:
- await resumeWithdrawalTransaction(ws, tx.withdrawalGroupId);
- return;
- case TransactionType.Payment:
- await resumePayMerchant(ws, tx.proposalId);
- return;
- case TransactionType.PeerPullCredit:
- await resumePeerPullCreditTransaction(ws, tx.pursePub);
- break;
- case TransactionType.PeerPushDebit:
- await resumePeerPushDebitTransaction(ws, tx.pursePub);
- break;
- case TransactionType.PeerPullDebit: {
- const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
- await ctx.resumeTransaction();
- return;
- }
- case TransactionType.PeerPushCredit:
- await resumePeerPushCreditTransaction(ws, tx.peerPushCreditId);
- break;
- case TransactionType.Refund:
- throw Error("refund transactions can't be suspended or resumed");
- case TransactionType.Reward:
- await resumeTipTransaction(ws, tx.walletRewardId);
- break;
- }
+ const ctx = await getContextForTransaction(ws, transactionId);
+ await ctx.resumeTransaction();
}
/**
@@ -1715,244 +1615,16 @@ export async function deleteTransaction(
ws: InternalWalletState,
transactionId: string,
): Promise<void> {
- const parsedTx = parseTransactionIdentifier(transactionId);
-
- if (!parsedTx) {
- throw Error("invalid transaction ID");
- }
-
- switch (parsedTx.tag) {
- case TransactionType.PeerPushCredit: {
- const peerPushCreditId = parsedTx.peerPushCreditId;
- await ws.db
- .mktx((x) => [x.withdrawalGroups, x.peerPushCredit, x.tombstones])
- .runReadWrite(async (tx) => {
- const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushInc) {
- return;
- }
- if (pushInc.withdrawalGroupId) {
- const withdrawalGroupId = pushInc.withdrawalGroupId;
- const withdrawalGroupRecord =
- await tx.withdrawalGroups.get(withdrawalGroupId);
- if (withdrawalGroupRecord) {
- await tx.withdrawalGroups.delete(withdrawalGroupId);
- await tx.tombstones.put({
- id:
- TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
- });
- }
- }
- await tx.peerPushCredit.delete(peerPushCreditId);
- await tx.tombstones.put({
- id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
- });
- });
- return;
- }
-
- case TransactionType.PeerPullCredit: {
- const pursePub = parsedTx.pursePub;
- await ws.db
- .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones])
- .runReadWrite(async (tx) => {
- const pullIni = await tx.peerPullCredit.get(pursePub);
- if (!pullIni) {
- return;
- }
- if (pullIni.withdrawalGroupId) {
- const withdrawalGroupId = pullIni.withdrawalGroupId;
- const withdrawalGroupRecord =
- await tx.withdrawalGroups.get(withdrawalGroupId);
- if (withdrawalGroupRecord) {
- await tx.withdrawalGroups.delete(withdrawalGroupId);
- await tx.tombstones.put({
- id:
- TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
- });
- }
- }
- await tx.peerPullCredit.delete(pursePub);
- await tx.tombstones.put({
- id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
- });
- });
-
- return;
- }
-
- case TransactionType.Withdrawal: {
- const withdrawalGroupId = parsedTx.withdrawalGroupId;
- await ws.db
- .mktx((x) => [x.withdrawalGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const withdrawalGroupRecord =
- await tx.withdrawalGroups.get(withdrawalGroupId);
- if (withdrawalGroupRecord) {
- await tx.withdrawalGroups.delete(withdrawalGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
- });
- return;
- }
- });
- return;
- }
-
- case TransactionType.Payment: {
- const proposalId = parsedTx.proposalId;
- await ws.db
- .mktx((x) => [x.purchases, x.tombstones])
- .runReadWrite(async (tx) => {
- let found = false;
- const purchase = await tx.purchases.get(proposalId);
- if (purchase) {
- found = true;
- await tx.purchases.delete(proposalId);
- }
- if (found) {
- await tx.tombstones.put({
- id: TombstoneTag.DeletePayment + ":" + proposalId,
- });
- }
- });
- return;
- }
-
- case TransactionType.Refresh: {
- const refreshGroupId = parsedTx.refreshGroupId;
- await ws.db
- .mktx((x) => [x.refreshGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (rg) {
- await tx.refreshGroups.delete(refreshGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
- });
- }
- });
-
- return;
- }
-
- case TransactionType.Reward: {
- const tipId = parsedTx.walletRewardId;
- await ws.db
- .mktx((x) => [x.rewards, x.tombstones])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.rewards.get(tipId);
- if (tipRecord) {
- await tx.rewards.delete(tipId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteReward + ":" + tipId,
- });
- }
- });
- return;
- }
-
- case TransactionType.Deposit: {
- const depositGroupId = parsedTx.depositGroupId;
- await deleteDepositGroup(ws, depositGroupId);
- return;
- }
-
- case TransactionType.Refund: {
- const refundGroupId = parsedTx.refundGroupId;
- await ws.db
- .mktx((x) => [x.refundGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const refundRecord = await tx.refundGroups.get(refundGroupId);
- if (!refundRecord) {
- return;
- }
- await tx.refundGroups.delete(refundGroupId);
- await tx.tombstones.put({ id: transactionId });
- // FIXME: Also tombstone the refund items, so that they won't reappear.
- });
- return;
- }
-
- case TransactionType.PeerPullDebit: {
- const peerPullDebitId = parsedTx.peerPullDebitId;
- await ws.db
- .mktx((x) => [x.peerPullDebit, x.tombstones])
- .runReadWrite(async (tx) => {
- const debit = await tx.peerPullDebit.get(peerPullDebitId);
- if (debit) {
- await tx.peerPullDebit.delete(peerPullDebitId);
- await tx.tombstones.put({ id: transactionId });
- }
- });
-
- return;
- }
-
- case TransactionType.PeerPushDebit: {
- const pursePub = parsedTx.pursePub;
- await ws.db
- .mktx((x) => [x.peerPushDebit, x.tombstones])
- .runReadWrite(async (tx) => {
- const debit = await tx.peerPushDebit.get(pursePub);
- if (debit) {
- await tx.peerPushDebit.delete(pursePub);
- await tx.tombstones.put({ id: transactionId });
- }
- });
- return;
- }
- }
+ const ctx = await getContextForTransaction(ws, transactionId);
+ await ctx.deleteTransaction();
}
export async function abortTransaction(
ws: InternalWalletState,
transactionId: string,
): Promise<void> {
- const txId = parseTransactionIdentifier(transactionId);
- if (!txId) {
- throw Error("invalid transaction identifier");
- }
-
- switch (txId.tag) {
- case TransactionType.Payment: {
- await abortPayMerchant(ws, txId.proposalId);
- break;
- }
- case TransactionType.Withdrawal:
- case TransactionType.InternalWithdrawal: {
- await abortWithdrawalTransaction(ws, txId.withdrawalGroupId);
- break;
- }
- case TransactionType.Deposit:
- await abortDepositGroup(ws, txId.depositGroupId);
- break;
- case TransactionType.Reward:
- await abortTipTransaction(ws, txId.walletRewardId);
- break;
- case TransactionType.Refund:
- throw Error("can't abort refund transactions");
- case TransactionType.Refresh:
- await abortRefreshGroup(ws, txId.refreshGroupId);
- break;
- case TransactionType.PeerPullCredit:
- await abortPeerPullCreditTransaction(ws, txId.pursePub);
- break;
- case TransactionType.PeerPullDebit: {
- const ctx = new PeerPullDebitTransactionContext(ws, txId.peerPullDebitId);
- await ctx.abortTransaction();
- return;
- }
- case TransactionType.PeerPushCredit:
- await abortPeerPushCreditTransaction(ws, txId.peerPushCreditId);
- break;
- case TransactionType.PeerPushDebit:
- await abortPeerPushDebitTransaction(ws, txId.pursePub);
- break;
- default: {
- assertUnreachable(txId);
- }
- }
+ const ctx = await getContextForTransaction(ws, transactionId);
+ await ctx.abortTransaction();
}
export interface TransitionInfo {
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index d02cf0597..58df75964 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -105,6 +105,8 @@ import {
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
constructTaskIdentifier,
makeCoinAvailable,
makeCoinsVisible,
@@ -146,246 +148,246 @@ import {
*/
const logger = new Logger("operations/withdraw.ts");
-export async function suspendWithdrawalTransaction(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.Withdraw,
- withdrawalGroupId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return;
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.PendingReady:
- newStatus = WithdrawalGroupStatus.SuspendedReady;
- break;
- case WithdrawalGroupStatus.AbortingBank:
- newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
- break;
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
- break;
- case WithdrawalGroupStatus.PendingRegisteringBank:
- newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
- break;
- case WithdrawalGroupStatus.PendingQueryingStatus:
- newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
- break;
- case WithdrawalGroupStatus.PendingKyc:
- newStatus = WithdrawalGroupStatus.SuspendedKyc;
- break;
- case WithdrawalGroupStatus.PendingAml:
- newStatus = WithdrawalGroupStatus.SuspendedAml;
- break;
- default:
- logger.warn(
- `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
- );
- }
- if (newStatus != null) {
- const oldTxState = computeWithdrawalTransactionStatus(wg);
- wg.status = newStatus;
- const newTxState = computeWithdrawalTransactionStatus(wg);
- await tx.withdrawalGroups.put(wg);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
+export class WithdrawTransactionContext implements TransactionContext {
+ public transactionId: string;
+ public retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public withdrawalGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
});
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Withdraw,
+ withdrawalGroupId,
+ });
+ }
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
+ async deleteTransaction(): Promise<void> {
+ const { ws, withdrawalGroupId } = this;
+ await ws.db
+ .mktx((x) => [x.withdrawalGroups, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (withdrawalGroupRecord) {
+ await tx.withdrawalGroups.delete(withdrawalGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+ });
+ return;
+ }
+ });
+ }
-export async function resumeWithdrawalTransaction(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-) {
- const transitionInfo = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return;
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.SuspendedReady:
- newStatus = WithdrawalGroupStatus.PendingReady;
- break;
- case WithdrawalGroupStatus.SuspendedAbortingBank:
- newStatus = WithdrawalGroupStatus.AbortingBank;
- break;
- case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
- newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
- break;
- case WithdrawalGroupStatus.SuspendedQueryingStatus:
- newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
- break;
- case WithdrawalGroupStatus.SuspendedRegisteringBank:
- newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
- break;
- case WithdrawalGroupStatus.SuspendedAml:
- newStatus = WithdrawalGroupStatus.PendingAml;
- break;
- case WithdrawalGroupStatus.SuspendedKyc:
- newStatus = WithdrawalGroupStatus.PendingKyc;
- break;
- default:
- logger.warn(
- `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`,
- );
- }
- if (newStatus != null) {
- const oldTxState = computeWithdrawalTransactionStatus(wg);
- wg.status = newStatus;
- const newTxState = computeWithdrawalTransactionStatus(wg);
- await tx.withdrawalGroups.put(wg);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
+ async suspendTransaction(): Promise<void> {
+ const { ws, withdrawalGroupId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.PendingReady:
+ newStatus = WithdrawalGroupStatus.SuspendedReady;
+ break;
+ case WithdrawalGroupStatus.AbortingBank:
+ newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
+ break;
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
+ break;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
+ break;
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
+ break;
+ case WithdrawalGroupStatus.PendingKyc:
+ newStatus = WithdrawalGroupStatus.SuspendedKyc;
+ break;
+ case WithdrawalGroupStatus.PendingAml:
+ newStatus = WithdrawalGroupStatus.SuspendedAml;
+ break;
+ default:
+ logger.warn(
+ `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
+ );
+ }
+ if (newStatus != null) {
+ const oldTxState = computeWithdrawalTransactionStatus(wg);
+ wg.status = newStatus;
+ const newTxState = computeWithdrawalTransactionStatus(wg);
+ await tx.withdrawalGroups.put(wg);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
-export async function abortWithdrawalTransaction(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.Withdraw,
- withdrawalGroupId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return;
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.SuspendedRegisteringBank:
- case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- case WithdrawalGroupStatus.PendingRegisteringBank:
- newStatus = WithdrawalGroupStatus.AbortingBank;
- break;
- case WithdrawalGroupStatus.SuspendedAml:
- case WithdrawalGroupStatus.SuspendedKyc:
- case WithdrawalGroupStatus.SuspendedQueryingStatus:
- case WithdrawalGroupStatus.SuspendedReady:
- case WithdrawalGroupStatus.PendingAml:
- case WithdrawalGroupStatus.PendingKyc:
- case WithdrawalGroupStatus.PendingQueryingStatus:
- newStatus = WithdrawalGroupStatus.AbortedExchange;
- break;
- case WithdrawalGroupStatus.PendingReady:
- newStatus = WithdrawalGroupStatus.SuspendedReady;
- break;
- case WithdrawalGroupStatus.SuspendedAbortingBank:
- case WithdrawalGroupStatus.AbortingBank:
- // No transition needed, but not an error
- break;
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.FailedBankAborted:
- case WithdrawalGroupStatus.AbortedExchange:
- case WithdrawalGroupStatus.AbortedBank:
- case WithdrawalGroupStatus.FailedAbortingBank:
- // Not allowed
- throw Error("abort not allowed in current state");
- break;
- default:
- assertUnreachable(wg.status);
- }
- if (newStatus != null) {
- const oldTxState = computeWithdrawalTransactionStatus(wg);
- wg.status = newStatus;
- const newTxState = computeWithdrawalTransactionStatus(wg);
- await tx.withdrawalGroups.put(wg);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
-export async function failWithdrawalTransaction(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.Withdraw,
- withdrawalGroupId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
- stopLongpolling(ws, taskId);
- const stateUpdate = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return;
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.SuspendedAbortingBank:
- case WithdrawalGroupStatus.AbortingBank:
- newStatus = WithdrawalGroupStatus.FailedAbortingBank;
- break;
- default:
- break;
- }
- if (newStatus != null) {
- const oldTxState = computeWithdrawalTransactionStatus(wg);
- wg.status = newStatus;
- const newTxState = computeWithdrawalTransactionStatus(wg);
- await tx.withdrawalGroups.put(wg);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, stateUpdate);
+ async abortTransaction(): Promise<void> {
+ const { ws, withdrawalGroupId, transactionId } = this;
+ stopLongpolling(ws, this.retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ newStatus = WithdrawalGroupStatus.AbortingBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedAml:
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ newStatus = WithdrawalGroupStatus.AbortedExchange;
+ break;
+ case WithdrawalGroupStatus.PendingReady:
+ newStatus = WithdrawalGroupStatus.SuspendedReady;
+ break;
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.AbortingBank:
+ // No transition needed, but not an error
+ break;
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ // Not allowed
+ throw Error("abort not allowed in current state");
+ break;
+ default:
+ assertUnreachable(wg.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeWithdrawalTransactionStatus(wg);
+ wg.status = newStatus;
+ const newTxState = computeWithdrawalTransactionStatus(wg);
+ await tx.withdrawalGroups.put(wg);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, withdrawalGroupId, transactionId } = this;
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedReady:
+ newStatus = WithdrawalGroupStatus.PendingReady;
+ break;
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ newStatus = WithdrawalGroupStatus.AbortingBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
+ break;
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedAml:
+ newStatus = WithdrawalGroupStatus.PendingAml;
+ break;
+ case WithdrawalGroupStatus.SuspendedKyc:
+ newStatus = WithdrawalGroupStatus.PendingKyc;
+ break;
+ default:
+ logger.warn(
+ `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`,
+ );
+ }
+ if (newStatus != null) {
+ const oldTxState = computeWithdrawalTransactionStatus(wg);
+ wg.status = newStatus;
+ const newTxState = computeWithdrawalTransactionStatus(wg);
+ await tx.withdrawalGroups.put(wg);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, withdrawalGroupId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const stateUpdate = await ws.db
+ .mktx((x) => [x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.AbortingBank:
+ newStatus = WithdrawalGroupStatus.FailedAbortingBank;
+ break;
+ default:
+ break;
+ }
+ if (newStatus != null) {
+ const oldTxState = computeWithdrawalTransactionStatus(wg);
+ wg.status = newStatus;
+ const newTxState = computeWithdrawalTransactionStatus(wg);
+ await tx.withdrawalGroups.put(wg);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, stateUpdate);
+ }
}
export function computeWithdrawalTransactionStatus(
@@ -2413,6 +2415,16 @@ export async function internalPerformCreateWithdrawalGroup(
exchangeNotif: undefined,
};
}
+ const existingWg = await tx.withdrawalGroups.get(
+ withdrawalGroup.withdrawalGroupId,
+ );
+ if (existingWg) {
+ return {
+ withdrawalGroup: existingWg,
+ exchangeNotif: undefined,
+ transitionInfo: undefined,
+ };
+ }
await tx.withdrawalGroups.add(withdrawalGroup);
await tx.reserves.put({
reservePub: withdrawalGroup.reservePub,
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
index 5d563f620..17b9b407c 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -581,7 +581,7 @@ export type DbReadOnlyTransactionArr<
}
: never;
-export interface TransactionContext<BoundStores> {
+export interface DbTransactionContext<BoundStores> {
runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
}
@@ -804,7 +804,7 @@ export class DbAccess<StoreMap> {
/**
* Run a transaction with all object stores.
*/
- mktxAll(): TransactionContext<StoreMap> {
+ mktxAll(): DbTransactionContext<StoreMap> {
const storeNames: string[] = [];
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
@@ -904,7 +904,7 @@ export class DbAccess<StoreMap> {
BoundStores extends {
[X in StoreNamesOf<StoreList>]: StoreList[number] & { storeName: X };
},
- >(namePicker: (x: StoreMap) => StoreList): TransactionContext<BoundStores> {
+ >(namePicker: (x: StoreMap) => StoreList): DbTransactionContext<BoundStores> {
const storeNames: string[] = [];
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 1fa1d117e..d6da2250a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -151,7 +151,6 @@ import {
CancelFn,
InternalWalletState,
MerchantInfo,
- MerchantOperations,
NotificationListener,
RecoupOperations,
RefreshOperations,
@@ -246,7 +245,7 @@ import {
import {
acceptTip,
computeRewardTransactionStatus,
- prepareTip,
+ prepareReward,
processTip,
} from "./operations/reward.js";
import {
@@ -1164,7 +1163,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.PrepareReward: {
const req = codecForPrepareRewardRequest().decode(payload);
- return await prepareTip(ws, req.talerRewardUri);
+ return await prepareReward(ws, req.talerRewardUri);
}
case WalletApiOperation.StartRefundQueryForUri: {
const req = codecForPrepareRefundRequest().decode(payload);
@@ -1609,9 +1608,6 @@ class InternalWalletStateImpl implements InternalWalletState {
createRecoupGroup,
};
- merchantOps: MerchantOperations = {
- getMerchantInfo,
- };
refreshOps: RefreshOperations = {
createRefreshGroup,