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, + transactionId: string, + ): Promise; + 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, + pendingTaskId: string, + e: TalerErrorDetail | undefined, +): Promise { + 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 { - 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); - } - await tx.operationRetries.put(retryRecord); - }); + 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 resetOperationTimeout( +export async function resetPendingTaskTimeout( + ws: InternalWalletState, + pendingTaskId: string, +): Promise { + 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); + } +} + +async function storePendingTaskPending( + ws: InternalWalletState, + pendingTaskId: string, +): Promise { + 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); + } +} + +async function storePendingTaskFinished( ws: InternalWalletState, pendingTaskId: string, ): Promise { 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); - } + await tx.operationRetries.delete(pendingTaskId); }); } -export async function storeOperationPending( - ws: InternalWalletState, - pendingTaskId: string, -): Promise { - 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); - }); -} - -export async function runOperationWithErrorReporting( +export async function runTaskWithErrorReporting( ws: InternalWalletState, opId: TaskId, f: () => Promise>, @@ -268,13 +399,13 @@ export async function runOperationWithErrorReporting( 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( 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( }, `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( {}, `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( } } -export async function storeOperationFinished( - ws: InternalWalletState, - pendingTaskId: string, -): Promise { - 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; process(): Promise; } + +export enum OperationAttemptResultType { + Finished = "finished", + Pending = "pending", + Error = "error", + Longpoll = "longpoll", +} + +export type OperationAttemptResult = + | OperationAttemptFinishedResult + | OperationAttemptErrorResult + | OperationAttemptLongpollResult + | OperationAttemptPendingResult; + +export namespace OperationAttemptResult { + export function finishedEmpty(): OperationAttemptResult { + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; + } + export function pendingEmpty(): OperationAttemptResult { + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } + export function longpoll(): OperationAttemptResult { + return { + type: OperationAttemptResultType.Longpoll, + }; + } +} + +export interface OperationAttemptFinishedResult { + type: OperationAttemptResultType.Finished; + result: T; +} + +export interface OperationAttemptPendingResult { + 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( + res: OperationAttemptResult, +): Promise { + 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, @@ -114,12 +117,6 @@ import { GetReadOnlyAccess, GetReadWriteAccess, } from "../util/query.js"; -import { - OperationAttemptResult, - OperationAttemptResultType, - TaskIdentifiers, - constructTaskIdentifier, -} from "../util/retries.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, @@ -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 = - | OperationAttemptFinishedResult - | OperationAttemptErrorResult - | OperationAttemptLongpollResult - | OperationAttemptPendingResult; - -export namespace OperationAttemptResult { - export function finishedEmpty(): OperationAttemptResult { - return { - type: OperationAttemptResultType.Finished, - result: undefined, - }; - } - export function pendingEmpty(): OperationAttemptResult { - return { - type: OperationAttemptResultType.Pending, - result: undefined, - }; - } - export function longpoll(): OperationAttemptResult { - return { - type: OperationAttemptResultType.Longpoll, - }; - } -} - -export interface OperationAttemptFinishedResult { - type: OperationAttemptResultType.Finished; - result: T; -} - -export interface OperationAttemptPendingResult { - 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 { - 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 { - 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( - res: OperationAttemptResult, -): Promise { - 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 { 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, + transactionId: string, + ): Promise { + 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<{