summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-06-20 11:40:06 +0200
committerFlorian Dold <florian@dold.me>2023-06-20 11:40:06 +0200
commit9c708251f92e6691ebba80fa8d129c6c04cec618 (patch)
treeedf46c7b3f9386697a4ea697c2d66f66323a6d3e
parent54f0c82999833132baf83995526025ac56d6fe06 (diff)
downloadwallet-core-9c708251f92e6691ebba80fa8d129c6c04cec618.tar.gz
wallet-core-9c708251f92e6691ebba80fa8d129c6c04cec618.tar.bz2
wallet-core-9c708251f92e6691ebba80fa8d129c6c04cec618.zip
wallet-core: emit DD37 self-transition notifications with errors
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts51
-rw-r--r--packages/taler-util/src/notifications.ts136
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/internal-wallet-state.ts9
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts31
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts62
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts581
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts26
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-common.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts27
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts9
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts31
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts26
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts32
-rw-r--r--packages/taler-wallet-core/src/pending-types.ts2
-rw-r--r--packages/taler-wallet-core/src/util/retries.ts306
-rw-r--r--packages/taler-wallet-core/src/wallet.ts108
24 files changed, 759 insertions, 728 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
index c98c18db5..d0515d64f 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -24,7 +24,14 @@ import {
BankApi,
BankAccessApi,
} from "@gnu-taler/taler-wallet-core";
-import { j2s, NotificationType, TransactionType, WithdrawalType } from "@gnu-taler/taler-util";
+import {
+ j2s,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -55,9 +62,22 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Withdraw
+ const r2 = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
(x) => {
- return x.type === NotificationType.WithdrawalGroupBankConfirmed;
+ return (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === r2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.ExchangeWaitReserve
+ );
},
);
@@ -67,15 +87,12 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
(x) => {
- return x.type === NotificationType.WithdrawalGroupReserveReady;
- },
- );
-
- const r2 = await walletClient.client.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
+ return (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === r2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.WithdrawCoins
+ );
},
);
@@ -99,7 +116,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
console.log("transactions before confirmation:", j2s(txn));
const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal);
- t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi);
+ t.assertTrue(
+ tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+ );
t.assertTrue(tx0.withdrawalDetails.confirmed === false);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
@@ -120,7 +139,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
console.log("transactions after confirmation:", j2s(txn));
const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal);
- t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi);
+ t.assertTrue(
+ tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+ );
t.assertTrue(tx0.withdrawalDetails.confirmed === true);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
}
@@ -138,7 +159,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
console.log("transactions after reserve ready:", j2s(txn));
const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal);
- t.assertTrue(tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi);
+ t.assertTrue(
+ tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+ );
t.assertTrue(tx0.withdrawalDetails.confirmed === true);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true);
}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index 51b56c3fe..b05fea8c9 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -36,43 +36,31 @@ export enum NotificationType {
RefreshMelted = "refresh-melted",
RefreshStarted = "refresh-started",
RefreshUnwarranted = "refresh-unwarranted",
- ReserveUpdated = "reserve-updated",
- ReserveConfirmed = "reserve-confirmed",
- ReserveCreated = "reserve-created",
WithdrawGroupCreated = "withdraw-group-created",
WithdrawGroupFinished = "withdraw-group-finished",
RefundStarted = "refund-started",
RefundQueried = "refund-queried",
ExchangeOperationError = "exchange-operation-error",
ExchangeAdded = "exchange-added",
- RefreshOperationError = "refresh-operation-error",
- RecoupOperationError = "recoup-operation-error",
- RefundApplyOperationError = "refund-apply-error",
- RefundStatusOperationError = "refund-status-error",
- ProposalOperationError = "proposal-error",
BackupOperationError = "backup-error",
- TipOperationError = "tip-error",
- PayOperationError = "pay-error",
- PayOperationSuccess = "pay-operation-success",
- WithdrawOperationError = "withdraw-error",
- ReserveNotYetFound = "reserve-not-yet-found",
- ReserveOperationError = "reserve-error",
InternalError = "internal-error",
PendingOperationProcessed = "pending-operation-processed",
- ProposalRefused = "proposal-refused",
- ReserveRegisteredWithBank = "reserve-registered-with-bank",
KycRequested = "kyc-requested",
- WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
- WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
- DepositOperationError = "deposit-operation-error",
TransactionStateTransition = "transaction-state-transition",
}
+export interface ErrorInfoSummary {
+ code: number;
+ hint?: string;
+ message?: string;
+}
+
export interface TransactionStateTransitionNotification {
type: NotificationType.TransactionStateTransition;
transactionId: string;
oldTxState: TransactionState;
newTxState: TransactionState;
+ errorInfo?: ErrorInfoSummary;
}
export interface ProposalAcceptedNotification {
@@ -86,11 +74,6 @@ export interface InternalErrorNotification {
exception: any;
}
-export interface ReserveNotYetFoundNotification {
- type: NotificationType.ReserveNotYetFound;
- reservePub: string;
-}
-
export interface CoinWithdrawnNotification {
type: NotificationType.CoinWithdrawn;
numWithdrawn: number;
@@ -137,16 +120,6 @@ export interface KycRequestedNotification {
kycUrl: string;
}
-export interface WithdrawalGroupBankConfirmed {
- type: NotificationType.WithdrawalGroupBankConfirmed;
- transactionId: string;
-}
-
-export interface WithdrawalGroupReserveReadyNotification {
- type: NotificationType.WithdrawalGroupReserveReady;
- transactionId: string;
-}
-
export interface RefreshRevealedNotification {
type: NotificationType.RefreshRevealed;
}
@@ -159,10 +132,6 @@ export interface RefreshRefusedNotification {
type: NotificationType.RefreshUnwarranted;
}
-export interface ReserveConfirmedNotification {
- type: NotificationType.ReserveConfirmed;
-}
-
export interface WithdrawalGroupCreatedNotification {
type: NotificationType.WithdrawGroupCreated;
withdrawalGroupId: string;
@@ -182,103 +151,22 @@ export interface ExchangeOperationErrorNotification {
error: TalerErrorDetail;
}
-export interface RefreshOperationErrorNotification {
- type: NotificationType.RefreshOperationError;
- error: TalerErrorDetail;
-}
-
export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError;
error: TalerErrorDetail;
}
-export interface RefundStatusOperationErrorNotification {
- type: NotificationType.RefundStatusOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RefundApplyOperationErrorNotification {
- type: NotificationType.RefundApplyOperationError;
- error: TalerErrorDetail;
-}
-
-export interface PayOperationErrorNotification {
- type: NotificationType.PayOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ProposalOperationErrorNotification {
- type: NotificationType.ProposalOperationError;
- error: TalerErrorDetail;
-}
-
-export interface TipOperationErrorNotification {
- type: NotificationType.TipOperationError;
- error: TalerErrorDetail;
-}
-
-export interface WithdrawOperationErrorNotification {
- type: NotificationType.WithdrawOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RecoupOperationErrorNotification {
- type: NotificationType.RecoupOperationError;
- error: TalerErrorDetail;
-}
-
-export interface DepositOperationErrorNotification {
- type: NotificationType.DepositOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ReserveOperationErrorNotification {
- type: NotificationType.ReserveOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ReserveCreatedNotification {
- type: NotificationType.ReserveCreated;
- reservePub: string;
-}
export interface PendingOperationProcessedNotification {
type: NotificationType.PendingOperationProcessed;
id: string;
}
-export interface ProposalRefusedNotification {
- type: NotificationType.ProposalRefused;
-}
-
-export interface ReserveRegisteredWithBankNotification {
- type: NotificationType.ReserveRegisteredWithBank;
-}
-
-/**
- * Notification sent when a pay (or pay replay) operation succeeded.
- *
- * We send this notification because the confirmPay request can return
- * a "confirmed" response that indicates that the payment has been confirmed
- * by the user, but we're still waiting for the payment to succeed or fail.
- */
-export interface PayOperationSuccessNotification {
- type: NotificationType.PayOperationSuccess;
- proposalId: string;
-}
export type WalletNotification =
| BackupOperationErrorNotification
- | WithdrawOperationErrorNotification
- | ReserveOperationErrorNotification
| ExchangeAddedNotification
| ExchangeOperationErrorNotification
- | RefreshOperationErrorNotification
- | RefundStatusOperationErrorNotification
- | RefundApplyOperationErrorNotification
- | ProposalOperationErrorNotification
- | PayOperationErrorNotification
- | TipOperationErrorNotification
| ProposalAcceptedNotification
| ProposalDownloadedNotification
| RefundsSubmittedNotification
@@ -288,22 +176,12 @@ export type WalletNotification =
| RefreshRevealedNotification
| RefreshStartedNotification
| RefreshRefusedNotification
- | ReserveCreatedNotification
- | ReserveConfirmedNotification
| WithdrawalGroupFinishedNotification
| RefundStartedNotification
| RefundQueriedNotification
| WithdrawalGroupCreatedNotification
| CoinWithdrawnNotification
- | RecoupOperationErrorNotification
- | DepositOperationErrorNotification
| InternalErrorNotification
| PendingOperationProcessedNotification
- | ProposalRefusedNotification
- | ReserveRegisteredWithBankNotification
- | ReserveNotYetFoundNotification
- | PayOperationSuccessNotification
| KycRequestedNotification
- | WithdrawalGroupBankConfirmed
- | WithdrawalGroupReserveReadyNotification
| TransactionStateTransitionNotification;
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 005b23985..3bf28aa94 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -70,7 +70,7 @@ import {
StoreDescriptor,
StoreWithIndexes,
} from "./util/query.js";
-import { RetryInfo, TaskIdentifiers } from "./util/retries.js";
+import { RetryInfo, TaskIdentifiers } from "./operations/common.js";
/**
* This file contains the database schema of the Taler wallet together
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts
index 8dc83c65a..d97703dc1 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -35,6 +35,7 @@ import {
DenominationInfo,
RefreshGroupId,
RefreshReason,
+ TransactionState,
WalletNotification,
} from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
@@ -145,7 +146,7 @@ export interface ActiveLongpollInfo {
}
/**
- * Internal, shard wallet state that is used by the implementation
+ * Internal, shared wallet state that is used by the implementation
* of wallet operations.
*
* FIXME: This should not be exported anywhere from the taler-wallet-core package,
@@ -183,6 +184,12 @@ export interface InternalWalletState {
merchantOps: MerchantOperations;
refreshOps: RefreshOperations;
+ getTransactionState(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ transactionId: string,
+ ): Promise<TransactionState | undefined>;
+
getDenomInfo(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index cda5a012b..7f73a14b0 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -62,7 +62,7 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
import { checkLogicInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import { makeCoinAvailable, makeTombstoneId, TombstoneTag } from "../common.js";
+import { constructTombstone, makeCoinAvailable, TombstoneTag } from "../common.js";
import { getExchangeDetails } from "../exchanges.js";
import { extractContractData } from "../pay-merchant.js";
import { provideBackupState } from "./state.js";
@@ -472,7 +472,10 @@ export async function importBackup(
for (const backupWg of backupBlob.withdrawal_groups) {
const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
checkLogicInvariant(!!reservePub);
- const ts = makeTombstoneId(TombstoneTag.DeleteReserve, reservePub);
+ const ts = constructTombstone({
+ tag: TombstoneTag.DeleteReserve,
+ reservePub,
+ });
if (tombstoneSet.has(ts)) {
continue;
}
@@ -558,10 +561,10 @@ export async function importBackup(
}
for (const backupPurchase of backupBlob.purchases) {
- const ts = makeTombstoneId(
- TombstoneTag.DeletePayment,
- backupPurchase.proposal_id,
- );
+ const ts = constructTombstone({
+ tag: TombstoneTag.DeletePayment,
+ proposalId: backupPurchase.proposal_id,
+ });
if (tombstoneSet.has(ts)) {
continue;
}
@@ -704,10 +707,10 @@ export async function importBackup(
}
for (const backupRefreshGroup of backupBlob.refresh_groups) {
- const ts = makeTombstoneId(
- TombstoneTag.DeleteRefreshGroup,
- backupRefreshGroup.refresh_group_id,
- );
+ const ts = constructTombstone({
+ tag: TombstoneTag.DeleteRefreshGroup,
+ refreshGroupId: backupRefreshGroup.refresh_group_id,
+ });
if (tombstoneSet.has(ts)) {
continue;
}
@@ -800,10 +803,10 @@ export async function importBackup(
}
for (const backupTip of backupBlob.tips) {
- const ts = makeTombstoneId(
- TombstoneTag.DeleteTip,
- backupTip.wallet_tip_id,
- );
+ const ts = constructTombstone({
+ tag: TombstoneTag.DeleteTip,
+ walletTipId: backupTip.wallet_tip_id,
+ });
if (tombstoneSet.has(ts)) {
continue;
}
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index f726167da..364e876ec 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -29,52 +29,52 @@ import {
AmountString,
AttentionType,
BackupRecovery,
+ Codec,
+ DenomKeyType,
+ EddsaKeyPair,
+ HttpStatusCode,
+ Logger,
+ PreparePayResult,
+ PreparePayResultType,
+ RecoveryLoadRequest,
+ RecoveryMergeStrategy,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ URL,
+ WalletBackupContentV1,
buildCodecForObject,
buildCodecForUnion,
bytesToString,
- canonicalizeBaseUrl,
canonicalJson,
- Codec,
+ canonicalizeBaseUrl,
codecForAmountString,
codecForBoolean,
codecForConstString,
codecForList,
codecForNumber,
codecForString,
- codecForTalerErrorDetail,
codecOptional,
- ConfirmPayResultType,
decodeCrock,
- DenomKeyType,
durationFromSpec,
eddsaGetPublic,
- EddsaKeyPair,
encodeCrock,
getRandomBytes,
hash,
hashDenomPub,
- HttpStatusCode,
j2s,
kdf,
- Logger,
notEmpty,
- PaymentStatus,
- PreparePayResult,
- PreparePayResultType,
- RecoveryLoadRequest,
- RecoveryMergeStrategy,
- ReserveTransactionType,
rsaBlind,
secretbox,
secretbox_open,
stringToBytes,
- TalerErrorCode,
- TalerErrorDetail,
- TalerProtocolTimestamp,
- TalerPreciseTimestamp,
- URL,
- WalletBackupContentV1,
} from "@gnu-taler/taler-util";
+import {
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
import { gunzipSync, gzipSync } from "fflate";
import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js";
import {
@@ -86,29 +86,19 @@ import {
ConfigRecordKey,
WalletBackupConfState,
} from "../../db.js";
-import { TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
import {
- readSuccessResponseJsonOrThrow,
- readTalerErrorResponse,
-} from "@gnu-taler/taler-util/http";
-import {
checkDbInvariant,
checkLogicInvariant,
} from "../../util/invariants.js";
+import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
- scheduleRetryInTx,
-} from "../../util/retries.js";
-import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
-import {
- checkPaymentByProposalId,
- confirmPay,
- preparePayForUri,
-} from "../pay-merchant.js";
+} from "../common.js";
+import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
import { exportBackup } from "./export.js";
import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
import { getWalletBackupState, provideBackupState } from "./state.js";
@@ -380,8 +370,6 @@ async function runBackupCycleForProvider(
logger.warn("backup provider not found anymore");
return;
}
- const opId = TaskIdentifiers.forBackup(prov);
- await scheduleRetryInTx(ws, tx, opId);
prov.shouldRetryFreshProposal = true;
prov.state = {
tag: BackupProviderStateTag.Retrying,
@@ -407,7 +395,7 @@ async function runBackupCycleForProvider(
return;
}
const opId = TaskIdentifiers.forBackup(prov);
- await scheduleRetryInTx(ws, tx, opId);
+ //await scheduleRetryInTx(ws, tx, opId);
prov.currentPaymentProposalId = result.proposalId;
prov.shouldRetryFreshProposal = false;
prov.state = {
@@ -481,7 +469,7 @@ async function runBackupCycleForProvider(
// FIXME: Allocate error code for this situation?
// FIXME: Add operation retry record!
const opId = TaskIdentifiers.forBackup(prov);
- await scheduleRetryInTx(ws, tx, opId);
+ //await scheduleRetryInTx(ws, tx, opId);
prov.state = {
tag: BackupProviderStateTag.Retrying,
};
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index ad18767c4..293870a18 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -18,42 +18,56 @@
* Imports.
*/
import {
+ AbsoluteTime,
AgeRestriction,
AmountJson,
Amounts,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
+ Duration,
+ ErrorInfoSummary,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
getErrorDetailFromException,
j2s,
Logger,
+ NotificationType,
OperationErrorInfo,
RefreshReason,
TalerErrorCode,
TalerErrorDetail,
TombstoneIdStr,
TransactionIdStr,
+ TransactionType,
+ WalletNotification,
} from "@gnu-taler/taler-util";
import {
WalletStoresV1,
CoinRecord,
ExchangeDetailsRecord,
ExchangeRecord,
+ BackupProviderRecord,
+ DepositGroupRecord,
+ PeerPullPaymentIncomingRecord,
+ PeerPullPaymentInitiationRecord,
+ PeerPushPaymentIncomingRecord,
+ PeerPushPaymentInitiationRecord,
+ PurchaseRecord,
+ RecoupGroupRecord,
+ RefreshGroupRecord,
+ TipRecord,
+ WithdrawalGroupRecord,
} from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { GetReadWriteAccess } from "../util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryInfo,
-} from "../util/retries.js";
+import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js";
-import { TaskId } from "../pending-types.js";
+import { PendingTaskType, TaskId } from "../pending-types.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+import { constructTransactionIdentifier } from "./transactions.js";
const logger = new Logger("operations/common.ts");
@@ -197,68 +211,185 @@ export async function spendCoins(
);
}
-export async function storeOperationError(
+/**
+ * Convert the task ID for a task that processes a transaction int
+ * the ID for the transaction.
+ */
+function convertTaskToTransactionId(
+ taskId: string,
+): TransactionIdStr | undefined {
+ const parsedTaskId = parseTaskIdentifier(taskId);
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.PeerPullCredit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: parsedTaskId.pursePub,
+ });
+ case PendingTaskType.PeerPullDebit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullPaymentIncomingId: parsedTaskId.peerPullPaymentIncomingId,
+ });
+ // FIXME: This doesn't distinguish internal-withdrawal.
+ // Maybe we should have a different task type for that as well?
+ // Or maybe transaction IDs should be valid task identifiers?
+ case PendingTaskType.Withdraw:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: parsedTaskId.withdrawalGroupId,
+ });
+ case PendingTaskType.PeerPushCredit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushPaymentIncomingId: parsedTaskId.peerPushPaymentIncomingId,
+ });
+ case PendingTaskType.Deposit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: parsedTaskId.depositGroupId,
+ });
+ case PendingTaskType.Refresh:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId: parsedTaskId.refreshGroupId,
+ });
+ case PendingTaskType.TipPickup:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Tip,
+ walletTipId: parsedTaskId.walletTipId,
+ });
+ case PendingTaskType.PeerPushDebit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: parsedTaskId.pursePub,
+ });
+ case PendingTaskType.Purchase:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: parsedTaskId.proposalId,
+ });
+ default:
+ return undefined;
+ }
+}
+
+/**
+ * For tasks that process a transaction,
+ * generate a state transition notification.
+ */
+async function taskToTransactionNotification(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ const txId = convertTaskToTransactionId(pendingTaskId);
+ if (!txId) {
+ return undefined;
+ }
+ const txState = await ws.getTransactionState(ws, tx, txId);
+ if (!txState) {
+ return undefined;
+ }
+ const notif: WalletNotification = {
+ type: NotificationType.TransactionStateTransition,
+ transactionId: txId,
+ oldTxState: txState,
+ newTxState: txState,
+ };
+ if (e) {
+ notif.errorInfo = {
+ code: e.code as number,
+ hint: e.hint,
+ };
+ }
+ return notif;
+}
+
+async function storePendingTaskError(
ws: InternalWalletState,
pendingTaskId: string,
e: TalerErrorDetail,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- let retryRecord = await tx.operationRetries.get(pendingTaskId);
- if (!retryRecord) {
- retryRecord = {
- id: pendingTaskId,
- lastError: e,
- retryInfo: RetryInfo.reset(),
- };
- } else {
- retryRecord.lastError = e;
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
- }
+ logger.info(`storing pending task error for ${pendingTaskId}`);
+ const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
+ let retryRecord = await tx.operationRetries.get(pendingTaskId);
+ if (!retryRecord) {
+ retryRecord = {
+ id: pendingTaskId,
+ lastError: e,
+ retryInfo: RetryInfo.reset(),
+ };
+ } else {
+ retryRecord.lastError = e;
+ retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+ }
+ await tx.operationRetries.put(retryRecord);
+ return taskToTransactionNotification(ws, tx, pendingTaskId, e);
+ });
+ if (maybeNotification) {
+ ws.notify(maybeNotification);
+ }
+}
+
+export async function resetPendingTaskTimeout(
+ ws: InternalWalletState,
+ pendingTaskId: string,
+): Promise<void> {
+ const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
+ let retryRecord = await tx.operationRetries.get(pendingTaskId);
+ if (retryRecord) {
+ // Note that we don't reset the lastError, it should still be visible
+ // while the retry runs.
+ retryRecord.retryInfo = RetryInfo.reset();
await tx.operationRetries.put(retryRecord);
- });
+ }
+ return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+ });
+ if (maybeNotification) {
+ ws.notify(maybeNotification);
+ }
}
-export async function resetOperationTimeout(
+async function storePendingTaskPending(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- let retryRecord = await tx.operationRetries.get(pendingTaskId);
- if (retryRecord) {
- // Note that we don't reset the lastError, it should still be visible
- // while the retry runs.
- retryRecord.retryInfo = RetryInfo.reset();
- await tx.operationRetries.put(retryRecord);
+ const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
+ let retryRecord = await tx.operationRetries.get(pendingTaskId);
+ let hadError = false;
+ if (!retryRecord) {
+ retryRecord = {
+ id: pendingTaskId,
+ retryInfo: RetryInfo.reset(),
+ };
+ } else {
+ if (retryRecord.lastError) {
+ hadError = true;
}
- });
+ delete retryRecord.lastError;
+ retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+ }
+ await tx.operationRetries.put(retryRecord);
+ return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+ });
+ if (maybeNotification) {
+ ws.notify(maybeNotification);
+ }
}
-export async function storeOperationPending(
+async function storePendingTaskFinished(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
await ws.db
.mktx((x) => [x.operationRetries])
.runReadWrite(async (tx) => {
- let retryRecord = await tx.operationRetries.get(pendingTaskId);
- if (!retryRecord) {
- retryRecord = {
- id: pendingTaskId,
- retryInfo: RetryInfo.reset(),
- };
- } else {
- delete retryRecord.lastError;
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
- }
- await tx.operationRetries.put(retryRecord);
+ await tx.operationRetries.delete(pendingTaskId);
});
}
-export async function runOperationWithErrorReporting<T1, T2>(
+export async function runTaskWithErrorReporting<T1, T2>(
ws: InternalWalletState,
opId: TaskId,
f: () => Promise<OperationAttemptResult<T1, T2>>,
@@ -268,13 +399,13 @@ export async function runOperationWithErrorReporting<T1, T2>(
const resp = await f();
switch (resp.type) {
case OperationAttemptResultType.Error:
- await storeOperationError(ws, opId, resp.errorDetail);
+ await storePendingTaskError(ws, opId, resp.errorDetail);
return resp;
case OperationAttemptResultType.Finished:
- await storeOperationFinished(ws, opId);
+ await storePendingTaskFinished(ws, opId);
return resp;
case OperationAttemptResultType.Pending:
- await storeOperationPending(ws, opId);
+ await storePendingTaskPending(ws, opId);
return resp;
case OperationAttemptResultType.Longpoll:
return resp;
@@ -297,7 +428,7 @@ export async function runOperationWithErrorReporting<T1, T2>(
logger.warn("operation processed resulted in error");
logger.warn(`error was: ${j2s(e.errorDetail)}`);
maybeError = e.errorDetail;
- await storeOperationError(ws, opId, maybeError!);
+ await storePendingTaskError(ws, opId, maybeError!);
return {
type: OperationAttemptResultType.Error,
errorDetail: e.errorDetail,
@@ -315,7 +446,7 @@ export async function runOperationWithErrorReporting<T1, T2>(
},
`unexpected exception (message: ${e.message})`,
);
- await storeOperationError(ws, opId, maybeError);
+ await storePendingTaskError(ws, opId, maybeError);
return {
type: OperationAttemptResultType.Error,
errorDetail: maybeError,
@@ -327,7 +458,7 @@ export async function runOperationWithErrorReporting<T1, T2>(
{},
`unexpected exception (not even an error)`,
);
- await storeOperationError(ws, opId, maybeError);
+ await storePendingTaskError(ws, opId, maybeError);
return {
type: OperationAttemptResultType.Error,
errorDetail: maybeError,
@@ -336,17 +467,6 @@ export async function runOperationWithErrorReporting<T1, T2>(
}
}
-export async function storeOperationFinished(
- ws: InternalWalletState,
- pendingTaskId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- await tx.operationRetries.delete(pendingTaskId);
- });
-}
-
export enum TombstoneTag {
DeleteWithdrawalGroup = "delete-withdrawal-group",
DeleteReserve = "delete-reserve",
@@ -361,15 +481,6 @@ export enum TombstoneTag {
DeletePeerPushCredit = "delete-peer-push-credit",
}
-/**
- * Create an event ID from the type and the primary key for the event.
- *
- * @deprecated use constructTombstone instead
- */
-export function makeTombstoneId(type: TombstoneTag, ...args: string[]): string {
- return `tmb:${type}:${args.map((x) => encodeURIComponent(x)).join(":")}`;
-}
-
export function getExchangeTosStatus(
exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus {
@@ -432,7 +543,7 @@ export function runLongpollAsync(
const asyncFn = async () => {
if (ws.stopped) {
logger.trace("not long-polling reserve, wallet already stopped");
- await storeOperationPending(ws, retryTag);
+ await storePendingTaskPending(ws, retryTag);
return;
}
const cts = CancellationToken.create();
@@ -446,13 +557,13 @@ export function runLongpollAsync(
};
res = await reqFn(cts.token);
} catch (e) {
- await storeOperationError(ws, retryTag, getErrorDetailFromException(e));
+ await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e));
return;
} finally {
delete ws.activeLongpoll[retryTag];
}
if (!res.ready) {
- await storeOperationPending(ws, retryTag);
+ await storePendingTaskPending(ws, retryTag);
}
ws.workAvailable.trigger();
};
@@ -464,7 +575,11 @@ export type ParsedTombstone =
tag: TombstoneTag.DeleteWithdrawalGroup;
withdrawalGroupId: string;
}
- | { tag: TombstoneTag.DeleteRefund; refundGroupId: string };
+ | { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
+ | { tag: TombstoneTag.DeleteReserve; reservePub: string }
+ | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
+ | { tag: TombstoneTag.DeleteTip; walletTipId: string }
+ | { tag: TombstoneTag.DeletePayment; proposalId: string };
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
switch (p.tag) {
@@ -472,6 +587,16 @@ export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteRefund:
return `tmb:${p.tag}:${p.refundGroupId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteReserve:
+ return `tmb:${p.tag}:${p.reservePub}` as TombstoneIdStr;
+ case TombstoneTag.DeletePayment:
+ return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteRefreshGroup:
+ return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteTip:
+ return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
+ default:
+ assertUnreachable(p);
}
}
@@ -487,3 +612,305 @@ export interface TransactionManager {
resume(): Promise<void>;
process(): Promise<OperationAttemptResult>;
}
+
+export enum OperationAttemptResultType {
+ Finished = "finished",
+ Pending = "pending",
+ Error = "error",
+ Longpoll = "longpoll",
+}
+
+export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
+ | OperationAttemptFinishedResult<TSuccess>
+ | OperationAttemptErrorResult
+ | OperationAttemptLongpollResult
+ | OperationAttemptPendingResult<TPending>;
+
+export namespace OperationAttemptResult {
+ export function finishedEmpty(): OperationAttemptResult<unknown, unknown> {
+ return {
+ type: OperationAttemptResultType.Finished,
+ result: undefined,
+ };
+ }
+ export function pendingEmpty(): OperationAttemptResult<unknown, unknown> {
+ return {
+ type: OperationAttemptResultType.Pending,
+ result: undefined,
+ };
+ }
+ export function longpoll(): OperationAttemptResult<unknown, unknown> {
+ return {
+ type: OperationAttemptResultType.Longpoll,
+ };
+ }
+}
+
+export interface OperationAttemptFinishedResult<T> {
+ type: OperationAttemptResultType.Finished;
+ result: T;
+}
+
+export interface OperationAttemptPendingResult<T> {
+ type: OperationAttemptResultType.Pending;
+ result: T;
+}
+
+export interface OperationAttemptErrorResult {
+ type: OperationAttemptResultType.Error;
+ errorDetail: TalerErrorDetail;
+}
+
+export interface OperationAttemptLongpollResult {
+ type: OperationAttemptResultType.Longpoll;
+}
+
+export interface RetryInfo {
+ firstTry: AbsoluteTime;
+ nextRetry: AbsoluteTime;
+ retryCounter: number;
+}
+
+export interface RetryPolicy {
+ readonly backoffDelta: Duration;
+ readonly backoffBase: number;
+ readonly maxTimeout: Duration;
+}
+
+const defaultRetryPolicy: RetryPolicy = {
+ backoffBase: 1.5,
+ backoffDelta: Duration.fromSpec({ seconds: 1 }),
+ maxTimeout: Duration.fromSpec({ minutes: 2 }),
+};
+
+function updateTimeout(
+ r: RetryInfo,
+ p: RetryPolicy = defaultRetryPolicy,
+): void {
+ const now = AbsoluteTime.now();
+ if (now.t_ms === "never") {
+ throw Error("assertion failed");
+ }
+ if (p.backoffDelta.d_ms === "forever") {
+ r.nextRetry = AbsoluteTime.never();
+ return;
+ }
+
+ const nextIncrement =
+ p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
+
+ const t =
+ now.t_ms +
+ (p.maxTimeout.d_ms === "forever"
+ ? nextIncrement
+ : Math.min(p.maxTimeout.d_ms, nextIncrement));
+ r.nextRetry = AbsoluteTime.fromMilliseconds(t);
+}
+
+export namespace RetryInfo {
+ export function getDuration(
+ r: RetryInfo | undefined,
+ p: RetryPolicy = defaultRetryPolicy,
+ ): Duration {
+ if (!r) {
+ // If we don't have any retry info, run immediately.
+ return { d_ms: 0 };
+ }
+ if (p.backoffDelta.d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
+ return {
+ d_ms:
+ p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
+ };
+ }
+
+ export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
+ const now = AbsoluteTime.now();
+ const info = {
+ firstTry: now,
+ nextRetry: now,
+ retryCounter: 0,
+ };
+ updateTimeout(info, p);
+ return info;
+ }
+
+ export function increment(
+ r: RetryInfo | undefined,
+ p: RetryPolicy = defaultRetryPolicy,
+ ): RetryInfo {
+ if (!r) {
+ return reset(p);
+ }
+ const r2 = { ...r };
+ r2.retryCounter++;
+ updateTimeout(r2, p);
+ return r2;
+ }
+}
+
+/**
+ * Parsed representation of task identifiers.
+ */
+export type ParsedTaskIdentifier =
+ | {
+ tag: PendingTaskType.Withdraw;
+ withdrawalGroupId: string;
+ }
+ | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
+ | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
+ | { tag: PendingTaskType.Deposit; depositGroupId: string }
+ | { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
+ | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
+ | { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string }
+ | { tag: PendingTaskType.PeerPullCredit; pursePub: string }
+ | { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string }
+ | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
+ | { tag: PendingTaskType.Purchase; proposalId: string }
+ | { tag: PendingTaskType.Recoup; recoupGroupId: string }
+ | { tag: PendingTaskType.TipPickup; walletTipId: string }
+ | { tag: PendingTaskType.Refresh; refreshGroupId: string };
+
+export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
+ const task = x.split(":");
+
+ if (task.length < 2) {
+ throw Error("task id should have al least 2 parts separated by ':'");
+ }
+
+ const [type, ...rest] = task;
+ switch (type) {
+ case PendingTaskType.Backup:
+ return { tag: type, backupProviderBaseUrl: rest[0] };
+ case PendingTaskType.Deposit:
+ return { tag: type, depositGroupId: rest[0] };
+ case PendingTaskType.ExchangeCheckRefresh:
+ return { tag: type, exchangeBaseUrl: rest[0] };
+ case PendingTaskType.ExchangeUpdate:
+ return { tag: type, exchangeBaseUrl: rest[0] };
+ case PendingTaskType.PeerPullCredit:
+ return { tag: type, pursePub: rest[0] };
+ case PendingTaskType.PeerPullDebit:
+ return { tag: type, peerPullPaymentIncomingId: rest[0] };
+ case PendingTaskType.PeerPushCredit:
+ return { tag: type, peerPushPaymentIncomingId: rest[0] };
+ case PendingTaskType.PeerPushDebit:
+ return { tag: type, pursePub: rest[0] };
+ case PendingTaskType.Purchase:
+ return { tag: type, proposalId: rest[0] };
+ case PendingTaskType.Recoup:
+ return { tag: type, recoupGroupId: rest[0] };
+ case PendingTaskType.Refresh:
+ return { tag: type, refreshGroupId: rest[0] };
+ case PendingTaskType.TipPickup:
+ return { tag: type, walletTipId: rest[0] };
+ case PendingTaskType.Withdraw:
+ return { tag: type, withdrawalGroupId: rest[0] };
+ default:
+ throw Error("invalid task identifier");
+ }
+}
+
+export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
+ switch (p.tag) {
+ case PendingTaskType.Backup:
+ return `${p.tag}:${p.backupProviderBaseUrl}` as TaskId;
+ case PendingTaskType.Deposit:
+ return `${p.tag}:${p.depositGroupId}` as TaskId;
+ case PendingTaskType.ExchangeCheckRefresh:
+ return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
+ case PendingTaskType.ExchangeUpdate:
+ return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
+ case PendingTaskType.PeerPullDebit:
+ return `${p.tag}:${p.peerPullPaymentIncomingId}` as TaskId;
+ case PendingTaskType.PeerPushCredit:
+ return `${p.tag}:${p.peerPushPaymentIncomingId}` as TaskId;
+ case PendingTaskType.PeerPullCredit:
+ return `${p.tag}:${p.pursePub}` as TaskId;
+ case PendingTaskType.PeerPushDebit:
+ return `${p.tag}:${p.pursePub}` as TaskId;
+ case PendingTaskType.Purchase:
+ return `${p.tag}:${p.proposalId}` as TaskId;
+ case PendingTaskType.Recoup:
+ return `${p.tag}:${p.recoupGroupId}` as TaskId;
+ case PendingTaskType.Refresh:
+ return `${p.tag}:${p.refreshGroupId}` as TaskId;
+ case PendingTaskType.TipPickup:
+ return `${p.tag}:${p.walletTipId}` as TaskId;
+ case PendingTaskType.Withdraw:
+ return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
+ default:
+ assertUnreachable(p);
+ }
+}
+
+export namespace TaskIdentifiers {
+ export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
+ return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
+ }
+ export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
+ return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
+ }
+ export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
+ return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
+ }
+ export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
+ return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
+ }
+ export function forTipPickup(tipRecord: TipRecord): TaskId {
+ return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId;
+ }
+ export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
+ return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId;
+ }
+ export function forPay(purchaseRecord: PurchaseRecord): TaskId {
+ return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as TaskId;
+ }
+ export function forRecoup(recoupRecord: RecoupGroupRecord): TaskId {
+ return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskId;
+ }
+ export function forDeposit(depositRecord: DepositGroupRecord): TaskId {
+ return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskId;
+ }
+ export function forBackup(backupRecord: BackupProviderRecord): TaskId {
+ return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
+ }
+ export function forPeerPushPaymentInitiation(
+ ppi: PeerPushPaymentInitiationRecord,
+ ): TaskId {
+ return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId;
+ }
+ export function forPeerPullPaymentInitiation(
+ ppi: PeerPullPaymentInitiationRecord,
+ ): TaskId {
+ return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId;
+ }
+ export function forPeerPullPaymentDebit(
+ ppi: PeerPullPaymentIncomingRecord,
+ ): TaskId {
+ return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}` as TaskId;
+ }
+ export function forPeerPushCredit(
+ ppi: PeerPushPaymentIncomingRecord,
+ ): TaskId {
+ return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}` as TaskId;
+ }
+}
+
+/**
+ * Run an operation handler, expect a success result and extract the success value.
+ */
+export async function unwrapOperationHandlerResultOrThrow<T>(
+ res: OperationAttemptResult<T>,
+): Promise<T> {
+ switch (res.type) {
+ case OperationAttemptResultType.Finished:
+ return res.result;
+ case OperationAttemptResultType.Error:
+ throw TalerError.fromUncheckedDetail(res.errorDetail);
+ default:
+ throw Error(`unexpected operation result (${res.type})`);
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 64180a3ea..6781696cf 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -79,8 +79,7 @@ import {
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { OperationAttemptResult } from "../util/retries.js";
-import { spendCoins, TombstoneTag } from "./common.js";
+import { constructTaskIdentifier, OperationAttemptResult, spendCoins, TombstoneTag } from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
import {
extractContractData,
@@ -94,7 +93,6 @@ import {
parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
-import { constructTaskIdentifier } from "../util/retries.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
/**
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 40ef22c6d..7e01071b4 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -74,14 +74,8 @@ import {
GetReadOnlyAccess,
GetReadWriteAccess,
} from "../util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- TaskIdentifiers,
- unwrapOperationHandlerResultOrThrow,
-} from "../util/retries.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
-import { runOperationWithErrorReporting } from "./common.js";
+import { OperationAttemptResult, OperationAttemptResultType, runTaskWithErrorReporting, TaskIdentifiers, unwrapOperationHandlerResultOrThrow } from "./common.js";
const logger = new Logger("exchanges.ts");
@@ -559,7 +553,7 @@ export async function updateExchangeFromUrl(
}> {
const canonUrl = canonicalizeBaseUrl(baseUrl);
return unwrapOperationHandlerResultOrThrow(
- await runOperationWithErrorReporting(
+ await runTaskWithErrorReporting(
ws,
TaskIdentifiers.forExchangeUpdateFromUrl(canonUrl),
() => updateExchangeFromUrlHandler(ws, canonUrl, options),
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index c3f288ff7..ad6552f06 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -67,7 +67,6 @@ import {
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
- TalerProtocolTimestamp,
TalerProtocolViolationError,
TalerUriAction,
TransactionAction,
@@ -116,12 +115,11 @@ import {
OperationAttemptResult,
OperationAttemptResultType,
RetryInfo,
- scheduleRetry,
TaskIdentifiers,
-} from "../util/retries.js";
+} from "./common.js";
import {
runLongpollAsync,
- runOperationWithErrorReporting,
+ runTaskWithErrorReporting,
spendCoins,
} from "./common.js";
import {
@@ -1254,7 +1252,7 @@ export async function runPayForConfirmPay(
tag: PendingTaskType.Purchase,
proposalId,
});
- const res = await runOperationWithErrorReporting(ws, taskId, async () => {
+ const res = await runTaskWithErrorReporting(ws, taskId, async () => {
return await processPurchasePay(ws, proposalId, { forceNow: true });
});
logger.trace(`processPurchasePay response type ${res.type}`);
@@ -1618,18 +1616,11 @@ export async function processPurchasePay(
// Do this in the background, as it might take some time
handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
console.log("handling insufficient funds failed");
-
- await scheduleRetry(ws, TaskIdentifiers.forPay(purchase), {
- code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- when: AbsoluteTime.now(),
- message: "unexpected exception",
- hint: "unexpected exception",
- details: {
- exception: e.toString(),
- },
- });
+ console.log(`${e.toString()}`);
});
+ // FIXME: Should we really consider this to be pending?
+
return {
type: OperationAttemptResultType.Pending,
result: undefined,
@@ -1694,11 +1685,6 @@ export async function processPurchasePay(
await unblockBackup(ws, proposalId);
}
- ws.notify({
- type: NotificationType.PayOperationSuccess,
- proposalId: purchase.proposalId,
- });
-
return OperationAttemptResult.finishedEmpty();
}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 4856fbe36..1bc2e8d49 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -52,11 +52,6 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { getTotalRefreshCost } from "./refresh.js";
-import {
- OperationAttemptLongpollResult,
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
const logger = new Logger("operations/peer-to-peer.ts");
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 48b81d6c2..5baba8cdc 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
@@ -66,12 +66,9 @@ import {
OperationAttemptResult,
OperationAttemptResultType,
constructTaskIdentifier,
-} from "../util/retries.js";
-import {
LongpollResult,
- resetOperationTimeout,
runLongpollAsync,
- runOperationWithErrorReporting,
+ runTaskWithErrorReporting,
} from "./common.js";
import {
codecForExchangePurseStatus,
@@ -486,26 +483,6 @@ export async function processPeerPullCredit(
switch (pullIni.status) {
case PeerPullPaymentInitiationStatus.Done: {
- // We implement this case so that the "retry" action on a peer-pull-credit transaction
- // also retries the withdrawal task.
-
- logger.warn(
- "peer pull payment initiation is already finished, retrying withdrawal",
- );
-
- const withdrawalGroupId = pullIni.withdrawalGroupId;
-
- if (withdrawalGroupId) {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.Withdraw,
- withdrawalGroupId,
- });
- stopLongpolling(ws, taskId);
- await resetOperationTimeout(ws, taskId);
- await runOperationWithErrorReporting(ws, taskId, () =>
- processWithdrawalGroup(ws, withdrawalGroupId),
- );
- }
return {
type: OperationAttemptResultType.Finished,
result: undefined,
@@ -811,7 +788,7 @@ export async function initiatePeerPullPayment(
pursePub: pursePair.pub,
});
- await runOperationWithErrorReporting(ws, taskId, async () => {
+ await runTaskWithErrorReporting(ws, taskId, async () => {
return processPeerPullCredit(ws, pursePair.pub);
});
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 0595a9e67..322d9ca71 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
@@ -59,13 +59,15 @@ import {
createRefreshGroup,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
+import { checkLogicInvariant } from "../util/invariants.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
constructTaskIdentifier,
-} from "../util/retries.js";
-import { runOperationWithErrorReporting, spendCoins } from "./common.js";
+ runTaskWithErrorReporting,
+ spendCoins,
+} from "./common.js";
import {
PeerCoinRepair,
codecForExchangePurseStatus,
@@ -78,7 +80,6 @@ import {
notifyTransition,
stopLongpolling,
} from "./transactions.js";
-import { checkLogicInvariant } from "../util/invariants.js";
const logger = new Logger("pay-peer-pull-debit.ts");
@@ -462,7 +463,7 @@ export async function confirmPeerPullDebit(
return pi;
});
- await runOperationWithErrorReporting(
+ await runTaskWithErrorReporting(
ws,
TaskIdentifiers.forPeerPullPaymentDebit(ppi),
async () => {
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 9b563b37e..cf698d512 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
@@ -60,12 +60,7 @@ import {
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant } from "../util/invariants.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- constructTaskIdentifier,
-} from "../util/retries.js";
-import { runLongpollAsync } from "./common.js";
+import { OperationAttemptResult, OperationAttemptResultType, constructTaskIdentifier, runLongpollAsync } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import {
codecForExchangePurseStatus,
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 fc7e868dc..c4209eb51 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
@@ -42,40 +42,41 @@ import {
j2s,
stringifyTalerUri,
} from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import {
- selectPeerCoins,
- getTotalPeerPaymentCost,
- codecForExchangePurseStatus,
- queryCoinInfosForSelection,
- PeerCoinRepair,
-} from "./pay-peer-common.js";
import {
HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
+import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
import {
PeerPushPaymentInitiationRecord,
PeerPushPaymentInitiationStatus,
RefreshOperationStatus,
createRefreshGroup,
} from "../index.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+import { checkLogicInvariant } from "../util/invariants.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
constructTaskIdentifier,
-} from "../util/retries.js";
-import { runLongpollAsync, spendCoins } from "./common.js";
+ runLongpollAsync,
+ spendCoins,
+} from "./common.js";
+import {
+ PeerCoinRepair,
+ codecForExchangePurseStatus,
+ getTotalPeerPaymentCost,
+ queryCoinInfosForSelection,
+ selectPeerCoins,
+} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
notifyTransition,
stopLongpolling,
} from "./transactions.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import { checkLogicInvariant } from "../util/invariants.js";
-import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
const logger = new Logger("pay-peer-push-debit.ts");
@@ -162,10 +163,10 @@ async function handlePurseCreationConflict(
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: {
const sel = coinSelRes.result;
- myPpi.coinSel = {
+ myPpi.coinSel = {
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution),
- }
+ };
break;
}
default:
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index a6450e08f..e7e7ffcfc 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -43,8 +43,8 @@ import {
import { AbsoluteTime } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { GetReadOnlyAccess } from "../util/query.js";
-import { TaskIdentifiers } from "../util/retries.js";
import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import { TaskIdentifiers } from "./common.js";
function getPendingCommon(
ws: InternalWalletState,
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index fcb7d6c98..71eb58ec9 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -53,12 +53,9 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { checkDbInvariant } from "../util/invariants.js";
import { GetReadWriteAccess } from "../util/query.js";
-import {
- OperationAttemptResult,
- unwrapOperationHandlerResultOrThrow,
-} from "../util/retries.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js";
+import { OperationAttemptResult } from "./common.js";
const logger = new Logger("operations/recoup.ts");
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index c2cf13857..e573ddb44 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -84,18 +84,12 @@ import {
} from "@gnu-taler/taler-util/http";
import { checkDbInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
-import {
- constructTaskIdentifier,
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
-import { makeCoinAvailable } from "./common.js";
+import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
import {
isWithdrawableDenom,
PendingTaskType,
- WalletConfig,
} from "../index.js";
import {
constructTransactionIdentifier,
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
index 1a565e02f..b43fd2e8a 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -57,12 +57,7 @@ import {
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import {
- constructTaskIdentifier,
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
-import { makeCoinAvailable } from "./common.js";
+import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import {
getCandidateWithdrawalDenoms,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index b6dc2e8bd..82b7cea64 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -69,8 +69,12 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { constructTaskIdentifier, TaskIdentifiers } from "../util/retries.js";
-import { resetOperationTimeout, TombstoneTag } from "./common.js";
+import {
+ constructTaskIdentifier,
+ resetPendingTaskTimeout,
+ TaskIdentifiers,
+ TombstoneTag,
+} from "./common.js";
import {
abortDepositGroup,
failDepositTransaction,
@@ -1388,7 +1392,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPullCredit,
pursePub: parsedTx.pursePub,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1397,7 +1401,7 @@ export async function retryTransaction(
tag: PendingTaskType.Deposit,
depositGroupId: parsedTx.depositGroupId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1408,7 +1412,7 @@ export async function retryTransaction(
tag: PendingTaskType.Withdraw,
withdrawalGroupId: parsedTx.withdrawalGroupId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1417,7 +1421,7 @@ export async function retryTransaction(
tag: PendingTaskType.Purchase,
proposalId: parsedTx.proposalId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1426,7 +1430,7 @@ export async function retryTransaction(
tag: PendingTaskType.TipPickup,
walletTipId: parsedTx.walletTipId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1435,7 +1439,7 @@ export async function retryTransaction(
tag: PendingTaskType.Refresh,
refreshGroupId: parsedTx.refreshGroupId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1444,7 +1448,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPullDebit,
peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1453,7 +1457,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPushCredit,
peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
@@ -1462,7 +1466,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPushDebit,
pursePub: parsedTx.pursePub,
});
- await resetOperationTimeout(ws, taskId);
+ await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId);
break;
}
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 88389fd99..dd07bdebc 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -92,10 +92,13 @@ import {
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import {
+ OperationAttemptResult,
+ OperationAttemptResultType,
+ TaskIdentifiers,
+ constructTaskIdentifier,
makeCoinAvailable,
makeExchangeListItem,
runLongpollAsync,
- runOperationWithErrorReporting,
} from "../operations/common.js";
import {
HttpRequestLibrary,
@@ -115,12 +118,6 @@ import {
GetReadWriteAccess,
} from "../util/query.js";
import {
- OperationAttemptResult,
- OperationAttemptResultType,
- TaskIdentifiers,
- constructTaskIdentifier,
-} from "../util/retries.js";
-import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
} from "../versions.js";
@@ -1225,10 +1222,6 @@ async function queryReserve(
result.talerErrorResponse.code ===
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) {
- ws.notify({
- type: NotificationType.ReserveNotYetFound,
- reservePub,
- });
return { ready: false };
} else {
throwUnexpectedRequestError(resp, result.talerErrorResponse);
@@ -1258,12 +1251,6 @@ async function queryReserve(
notifyTransition(ws, transactionId, transitionResult);
- // FIXME: This notification is deprecated with DD37
- ws.notify({
- type: NotificationType.WithdrawalGroupReserveReady,
- transactionId,
- });
-
return { ready: true };
}
@@ -2053,8 +2040,6 @@ async function registerReserveWithBank(
});
notifyTransition(ws, transactionId, transitionInfo);
- // FIXME: This notification is deprecated with DD37
- ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
}
interface BankStatusResult {
@@ -2176,15 +2161,6 @@ async function processReserveBankStatus(
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.PendingQueryingStatus;
- // FIXME: Notification is deprecated with DD37.
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: r.withdrawalGroupId,
- });
- ws.notify({
- type: NotificationType.WithdrawalGroupBankConfirmed,
- transactionId,
- });
} else {
logger.info("withdrawal: transfer not yet confirmed by bank");
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts
index e85f0d460..3bb6636ee 100644
--- a/packages/taler-wallet-core/src/pending-types.ts
+++ b/packages/taler-wallet-core/src/pending-types.ts
@@ -25,7 +25,7 @@
* Imports.
*/
import { TalerErrorDetail, AbsoluteTime } from "@gnu-taler/taler-util";
-import { RetryInfo } from "./util/retries.js";
+import { RetryInfo } from "./operations/common.js";
export enum PendingTaskType {
ExchangeUpdate = "exchange-update",
diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts
index e85eb0a6b..e602d4702 100644
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ b/packages/taler-wallet-core/src/util/retries.ts
@@ -50,309 +50,3 @@ import { assertUnreachable } from "./assertUnreachable.js";
const logger = new Logger("util/retries.ts");
-export enum OperationAttemptResultType {
- Finished = "finished",
- Pending = "pending",
- Error = "error",
- Longpoll = "longpoll",
-}
-
-export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
- | OperationAttemptFinishedResult<TSuccess>
- | OperationAttemptErrorResult
- | OperationAttemptLongpollResult
- | OperationAttemptPendingResult<TPending>;
-
-export namespace OperationAttemptResult {
- export function finishedEmpty(): OperationAttemptResult<unknown, unknown> {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
- export function pendingEmpty(): OperationAttemptResult<unknown, unknown> {
- return {
- type: OperationAttemptResultType.Pending,
- result: undefined,
- };
- }
- export function longpoll(): OperationAttemptResult<unknown, unknown> {
- return {
- type: OperationAttemptResultType.Longpoll,
- };
- }
-}
-
-export interface OperationAttemptFinishedResult<T> {
- type: OperationAttemptResultType.Finished;
- result: T;
-}
-
-export interface OperationAttemptPendingResult<T> {
- type: OperationAttemptResultType.Pending;
- result: T;
-}
-
-export interface OperationAttemptErrorResult {
- type: OperationAttemptResultType.Error;
- errorDetail: TalerErrorDetail;
-}
-
-export interface OperationAttemptLongpollResult {
- type: OperationAttemptResultType.Longpoll;
-}
-
-export interface RetryInfo {
- firstTry: AbsoluteTime;
- nextRetry: AbsoluteTime;
- retryCounter: number;
-}
-
-export interface RetryPolicy {
- readonly backoffDelta: Duration;
- readonly backoffBase: number;
- readonly maxTimeout: Duration;
-}
-
-const defaultRetryPolicy: RetryPolicy = {
- backoffBase: 1.5,
- backoffDelta: Duration.fromSpec({ seconds: 1 }),
- maxTimeout: Duration.fromSpec({ minutes: 2 }),
-};
-
-function updateTimeout(
- r: RetryInfo,
- p: RetryPolicy = defaultRetryPolicy,
-): void {
- const now = AbsoluteTime.now();
- if (now.t_ms === "never") {
- throw Error("assertion failed");
- }
- if (p.backoffDelta.d_ms === "forever") {
- r.nextRetry = AbsoluteTime.never();
- return;
- }
-
- const nextIncrement =
- p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
-
- const t =
- now.t_ms +
- (p.maxTimeout.d_ms === "forever"
- ? nextIncrement
- : Math.min(p.maxTimeout.d_ms, nextIncrement));
- r.nextRetry = AbsoluteTime.fromMilliseconds(t);
-}
-
-export namespace RetryInfo {
- export function getDuration(
- r: RetryInfo | undefined,
- p: RetryPolicy = defaultRetryPolicy,
- ): Duration {
- if (!r) {
- // If we don't have any retry info, run immediately.
- return { d_ms: 0 };
- }
- if (p.backoffDelta.d_ms === "forever") {
- return { d_ms: "forever" };
- }
- const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
- return {
- d_ms:
- p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
- };
- }
-
- export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
- const now = AbsoluteTime.now();
- const info = {
- firstTry: now,
- nextRetry: now,
- retryCounter: 0,
- };
- updateTimeout(info, p);
- return info;
- }
-
- export function increment(
- r: RetryInfo | undefined,
- p: RetryPolicy = defaultRetryPolicy,
- ): RetryInfo {
- if (!r) {
- return reset(p);
- }
- const r2 = { ...r };
- r2.retryCounter++;
- updateTimeout(r2, p);
- return r2;
- }
-}
-
-/**
- * Parsed representation of task identifiers.
- */
-export type ParsedTaskIdentifier =
- | {
- tag: PendingTaskType.Withdraw;
- withdrawalGroupId: string;
- }
- | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
- | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
- | { tag: PendingTaskType.Deposit; depositGroupId: string }
- | { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
- | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
- | { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string }
- | { tag: PendingTaskType.PeerPullCredit; pursePub: string }
- | { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string }
- | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
- | { tag: PendingTaskType.Purchase; proposalId: string }
- | { tag: PendingTaskType.Recoup; recoupGroupId: string }
- | { tag: PendingTaskType.TipPickup; walletTipId: string }
- | { tag: PendingTaskType.Refresh; refreshGroupId: string };
-
-export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
- throw Error("not yet implemented");
-}
-
-export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
- switch (p.tag) {
- case PendingTaskType.Backup:
- return `${p.tag}:${p.backupProviderBaseUrl}` as TaskId;
- case PendingTaskType.Deposit:
- return `${p.tag}:${p.depositGroupId}` as TaskId;
- case PendingTaskType.ExchangeCheckRefresh:
- return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
- case PendingTaskType.ExchangeUpdate:
- return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
- case PendingTaskType.PeerPullDebit:
- return `${p.tag}:${p.peerPullPaymentIncomingId}` as TaskId;
- case PendingTaskType.PeerPushCredit:
- return `${p.tag}:${p.peerPushPaymentIncomingId}` as TaskId;
- case PendingTaskType.PeerPullCredit:
- return `${p.tag}:${p.pursePub}` as TaskId;
- case PendingTaskType.PeerPushDebit:
- return `${p.tag}:${p.pursePub}` as TaskId;
- case PendingTaskType.Purchase:
- return `${p.tag}:${p.proposalId}` as TaskId;
- case PendingTaskType.Recoup:
- return `${p.tag}:${p.recoupGroupId}` as TaskId;
- case PendingTaskType.Refresh:
- return `${p.tag}:${p.refreshGroupId}` as TaskId;
- case PendingTaskType.TipPickup:
- return `${p.tag}:${p.walletTipId}` as TaskId;
- case PendingTaskType.Withdraw:
- return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
- default:
- assertUnreachable(p);
- }
-}
-
-export namespace TaskIdentifiers {
- export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
- return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
- }
- export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
- return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
- }
- export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
- return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
- }
- export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
- return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
- }
- export function forTipPickup(tipRecord: TipRecord): TaskId {
- return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId;
- }
- export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
- return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId;
- }
- export function forPay(purchaseRecord: PurchaseRecord): TaskId {
- return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as TaskId;
- }
- export function forRecoup(recoupRecord: RecoupGroupRecord): TaskId {
- return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskId;
- }
- export function forDeposit(depositRecord: DepositGroupRecord): TaskId {
- return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskId;
- }
- export function forBackup(backupRecord: BackupProviderRecord): TaskId {
- return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
- }
- export function forPeerPushPaymentInitiation(
- ppi: PeerPushPaymentInitiationRecord,
- ): TaskId {
- return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId;
- }
- export function forPeerPullPaymentInitiation(
- ppi: PeerPullPaymentInitiationRecord,
- ): TaskId {
- return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId;
- }
- export function forPeerPullPaymentDebit(
- ppi: PeerPullPaymentIncomingRecord,
- ): TaskId {
- return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}` as TaskId;
- }
- export function forPeerPushCredit(
- ppi: PeerPushPaymentIncomingRecord,
- ): TaskId {
- return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}` as TaskId;
- }
-}
-
-export async function scheduleRetryInTx(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- opId: string,
- errorDetail?: TalerErrorDetail,
-): Promise<void> {
- let retryRecord = await tx.operationRetries.get(opId);
- if (!retryRecord) {
- retryRecord = {
- id: opId,
- retryInfo: RetryInfo.reset(),
- };
- if (errorDetail) {
- retryRecord.lastError = errorDetail;
- }
- } else {
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
- if (errorDetail) {
- retryRecord.lastError = errorDetail;
- } else {
- delete retryRecord.lastError;
- }
- }
- await tx.operationRetries.put(retryRecord);
-}
-
-export async function scheduleRetry(
- ws: InternalWalletState,
- opId: string,
- errorDetail?: TalerErrorDetail,
-): Promise<void> {
- return await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- tx.operationRetries;
- scheduleRetryInTx(ws, tx, opId, errorDetail);
- });
-}
-
-/**
- * Run an operation handler, expect a success result and extract the success value.
- */
-export async function unwrapOperationHandlerResultOrThrow<T>(
- res: OperationAttemptResult<T>,
-): Promise<T> {
- switch (res.type) {
- case OperationAttemptResultType.Finished:
- return res.result;
- case OperationAttemptResultType.Error:
- throw TalerError.fromUncheckedDetail(res.errorDetail);
- default:
- throw Error(`unexpected operation result (${res.type})`);
- }
-}
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index a04464630..e5cd713b8 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -48,6 +48,7 @@ import {
RefreshReason,
TalerError,
TalerErrorCode,
+ TransactionState,
TransactionType,
URL,
ValidateIbanResponse,
@@ -170,9 +171,10 @@ import { getBalanceDetail, getBalances } from "./operations/balance.js";
import {
getExchangeTosStatus,
makeExchangeListItem,
- runOperationWithErrorReporting,
+ runTaskWithErrorReporting,
} from "./operations/common.js";
import {
+ computeDepositTransactionStatus,
createDepositGroup,
generateDepositGroupTxId,
prepareDepositGroup,
@@ -191,6 +193,9 @@ import {
} from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js";
import {
+ computePayMerchantTransactionActions,
+ computePayMerchantTransactionState,
+ computeRefundTransactionState,
confirmPay,
getContractTermsDetails,
preparePayForUri,
@@ -200,21 +205,25 @@ import {
} from "./operations/pay-merchant.js";
import {
checkPeerPullPaymentInitiation,
+ computePeerPullCreditTransactionState,
initiatePeerPullPayment,
processPeerPullCredit,
} from "./operations/pay-peer-pull-credit.js";
import {
+ computePeerPullDebitTransactionState,
confirmPeerPullDebit,
preparePeerPullDebit,
processPeerPullDebit,
} from "./operations/pay-peer-pull-debit.js";
import {
+ computePeerPushCreditTransactionState,
confirmPeerPushCredit,
preparePeerPushCredit,
processPeerPushCredit,
} from "./operations/pay-peer-push-credit.js";
import {
checkPeerPushDebit,
+ computePeerPushDebitTransactionState,
initiatePeerPushDebit,
processPeerPushDebit,
} from "./operations/pay-peer-push-debit.js";
@@ -222,6 +231,7 @@ import { getPendingOperations } from "./operations/pending.js";
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
import {
autoRefresh,
+ computeRefreshTransactionState,
createRefreshGroup,
processRefreshGroup,
} from "./operations/refresh.js";
@@ -231,7 +241,7 @@ import {
testPay,
withdrawTestBalance,
} from "./operations/testing.js";
-import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
+import { acceptTip, computeTipTransactionStatus, prepareTip, processTip } from "./operations/tip.js";
import {
abortTransaction,
deleteTransaction,
@@ -245,6 +255,7 @@ import {
} from "./operations/transactions.js";
import {
acceptWithdrawalFromUri,
+ computeWithdrawalTransactionStatus,
createManualWithdrawal,
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
@@ -268,7 +279,7 @@ import {
GetReadOnlyAccess,
GetReadWriteAccess,
} from "./util/query.js";
-import { OperationAttemptResult, TaskIdentifiers } from "./util/retries.js";
+import { OperationAttemptResult, TaskIdentifiers } from "./operations/common.js";
import { TimerAPI, TimerGroup } from "./util/timer.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -337,7 +348,7 @@ export async function runPending(ws: InternalWalletState): Promise<void> {
if (!AbsoluteTime.isExpired(p.timestampDue)) {
continue;
}
- await runOperationWithErrorReporting(ws, p.id, async () => {
+ await runTaskWithErrorReporting(ws, p.id, async () => {
logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
return await callOperationHandler(ws, p);
});
@@ -439,7 +450,7 @@ async function runTaskLoop(
if (!AbsoluteTime.isExpired(p.timestampDue)) {
continue;
}
- await runOperationWithErrorReporting(ws, p.id, async () => {
+ await runTaskWithErrorReporting(ws, p.id, async () => {
logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
return await callOperationHandler(ws, p);
});
@@ -1711,6 +1722,93 @@ class InternalWalletStateImpl implements InternalWalletState {
}
}
+ async getTransactionState(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ transactionId: string,
+ ): Promise<TransactionState | undefined> {
+ const parsedTxId = parseTransactionIdentifier(transactionId);
+ if (!parsedTxId) {
+ throw Error("invalid tx identifier");
+ }
+ switch (parsedTxId.tag) {
+ case TransactionType.Deposit: {
+ const rec = await tx.depositGroups.get(parsedTxId.depositGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeDepositTransactionStatus(rec);
+ }
+ case TransactionType.InternalWithdrawal:
+ case TransactionType.Withdrawal: {
+ const rec = await tx.withdrawalGroups.get(parsedTxId.withdrawalGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeWithdrawalTransactionStatus(rec);
+ }
+ case TransactionType.Payment: {
+ const rec = await tx.purchases.get(parsedTxId.proposalId);
+ if (!rec) {
+ return;
+ }
+ return computePayMerchantTransactionState(rec);
+ }
+ case TransactionType.Refund: {
+ const rec = await tx.refundGroups.get(
+ parsedTxId.refundGroupId,
+ );
+ if (!rec) {
+ return undefined;
+ }
+ return computeRefundTransactionState(rec);
+ }
+ case TransactionType.PeerPullCredit:
+ const rec = await tx.peerPullPaymentInitiations.get(parsedTxId.pursePub);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPullCreditTransactionState(rec);
+ case TransactionType.PeerPullDebit: {
+ const rec = await tx.peerPullPaymentIncoming.get(parsedTxId.peerPullPaymentIncomingId);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPullDebitTransactionState(rec);
+ }
+ case TransactionType.PeerPushCredit: {
+ const rec = await tx.peerPushPaymentIncoming.get(parsedTxId.peerPushPaymentIncomingId);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPushCreditTransactionState(rec);
+ }
+ case TransactionType.PeerPushDebit: {
+ const rec = await tx.peerPushPaymentInitiations.get(parsedTxId.pursePub);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPushDebitTransactionState(rec);
+ }
+ case TransactionType.Refresh: {
+ const rec = await tx.refreshGroups.get(parsedTxId.refreshGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeRefreshTransactionState(rec)
+ }
+ case TransactionType.Tip: {
+ const rec = await tx.tips.get(parsedTxId.walletTipId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeTipTransactionStatus(rec);
+ }
+ default:
+ assertUnreachable(parsedTxId);
+ }
+ }
+
async getDenomInfo(
ws: InternalWalletState,
tx: GetReadWriteAccess<{