wallet-core: emit DD37 self-transition notifications with errors

This commit is contained in:
Florian Dold 2023-06-20 11:40:06 +02:00
parent 54f0c82999
commit 9c708251f9
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
24 changed files with 766 additions and 735 deletions

View File

@ -24,7 +24,14 @@ import {
BankApi, BankApi,
BankAccessApi, BankAccessApi,
} from "@gnu-taler/taler-wallet-core"; } 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. * Run test for basic, bank-integrated withdrawal.
@ -55,9 +62,22 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Withdraw // Withdraw
const r2 = await walletClient.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond( const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
(x) => { (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( const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
(x) => { (x) => {
return x.type === NotificationType.WithdrawalGroupReserveReady; return (
}, x.type === NotificationType.TransactionStateTransition &&
); x.transactionId === r2.transactionId &&
x.newTxState.major === TransactionMajorState.Pending &&
const r2 = await walletClient.client.call( x.newTxState.minor === TransactionMinorState.WithdrawCoins
WalletApiOperation.AcceptBankIntegratedWithdrawal, );
{
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
}, },
); );
@ -99,7 +116,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
console.log("transactions before confirmation:", j2s(txn)); console.log("transactions before confirmation:", j2s(txn));
const tx0 = txn.transactions[0]; const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal); 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.confirmed === false);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false); t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
} }
@ -120,7 +139,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
console.log("transactions after confirmation:", j2s(txn)); console.log("transactions after confirmation:", j2s(txn));
const tx0 = txn.transactions[0]; const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal); 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.confirmed === true);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false); t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
} }
@ -138,7 +159,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
console.log("transactions after reserve ready:", j2s(txn)); console.log("transactions after reserve ready:", j2s(txn));
const tx0 = txn.transactions[0]; const tx0 = txn.transactions[0];
t.assertTrue(tx0.type === TransactionType.Withdrawal); 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.confirmed === true);
t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true); t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true);
} }

View File

@ -36,43 +36,31 @@ export enum NotificationType {
RefreshMelted = "refresh-melted", RefreshMelted = "refresh-melted",
RefreshStarted = "refresh-started", RefreshStarted = "refresh-started",
RefreshUnwarranted = "refresh-unwarranted", RefreshUnwarranted = "refresh-unwarranted",
ReserveUpdated = "reserve-updated",
ReserveConfirmed = "reserve-confirmed",
ReserveCreated = "reserve-created",
WithdrawGroupCreated = "withdraw-group-created", WithdrawGroupCreated = "withdraw-group-created",
WithdrawGroupFinished = "withdraw-group-finished", WithdrawGroupFinished = "withdraw-group-finished",
RefundStarted = "refund-started", RefundStarted = "refund-started",
RefundQueried = "refund-queried", RefundQueried = "refund-queried",
ExchangeOperationError = "exchange-operation-error", ExchangeOperationError = "exchange-operation-error",
ExchangeAdded = "exchange-added", 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", 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", InternalError = "internal-error",
PendingOperationProcessed = "pending-operation-processed", PendingOperationProcessed = "pending-operation-processed",
ProposalRefused = "proposal-refused",
ReserveRegisteredWithBank = "reserve-registered-with-bank",
KycRequested = "kyc-requested", KycRequested = "kyc-requested",
WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
DepositOperationError = "deposit-operation-error",
TransactionStateTransition = "transaction-state-transition", TransactionStateTransition = "transaction-state-transition",
} }
export interface ErrorInfoSummary {
code: number;
hint?: string;
message?: string;
}
export interface TransactionStateTransitionNotification { export interface TransactionStateTransitionNotification {
type: NotificationType.TransactionStateTransition; type: NotificationType.TransactionStateTransition;
transactionId: string; transactionId: string;
oldTxState: TransactionState; oldTxState: TransactionState;
newTxState: TransactionState; newTxState: TransactionState;
errorInfo?: ErrorInfoSummary;
} }
export interface ProposalAcceptedNotification { export interface ProposalAcceptedNotification {
@ -86,11 +74,6 @@ export interface InternalErrorNotification {
exception: any; exception: any;
} }
export interface ReserveNotYetFoundNotification {
type: NotificationType.ReserveNotYetFound;
reservePub: string;
}
export interface CoinWithdrawnNotification { export interface CoinWithdrawnNotification {
type: NotificationType.CoinWithdrawn; type: NotificationType.CoinWithdrawn;
numWithdrawn: number; numWithdrawn: number;
@ -137,16 +120,6 @@ export interface KycRequestedNotification {
kycUrl: string; kycUrl: string;
} }
export interface WithdrawalGroupBankConfirmed {
type: NotificationType.WithdrawalGroupBankConfirmed;
transactionId: string;
}
export interface WithdrawalGroupReserveReadyNotification {
type: NotificationType.WithdrawalGroupReserveReady;
transactionId: string;
}
export interface RefreshRevealedNotification { export interface RefreshRevealedNotification {
type: NotificationType.RefreshRevealed; type: NotificationType.RefreshRevealed;
} }
@ -159,10 +132,6 @@ export interface RefreshRefusedNotification {
type: NotificationType.RefreshUnwarranted; type: NotificationType.RefreshUnwarranted;
} }
export interface ReserveConfirmedNotification {
type: NotificationType.ReserveConfirmed;
}
export interface WithdrawalGroupCreatedNotification { export interface WithdrawalGroupCreatedNotification {
type: NotificationType.WithdrawGroupCreated; type: NotificationType.WithdrawGroupCreated;
withdrawalGroupId: string; withdrawalGroupId: string;
@ -182,103 +151,22 @@ export interface ExchangeOperationErrorNotification {
error: TalerErrorDetail; error: TalerErrorDetail;
} }
export interface RefreshOperationErrorNotification {
type: NotificationType.RefreshOperationError;
error: TalerErrorDetail;
}
export interface BackupOperationErrorNotification { export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError; type: NotificationType.BackupOperationError;
error: TalerErrorDetail; 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 { export interface PendingOperationProcessedNotification {
type: NotificationType.PendingOperationProcessed; type: NotificationType.PendingOperationProcessed;
id: string; 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 = export type WalletNotification =
| BackupOperationErrorNotification | BackupOperationErrorNotification
| WithdrawOperationErrorNotification
| ReserveOperationErrorNotification
| ExchangeAddedNotification | ExchangeAddedNotification
| ExchangeOperationErrorNotification | ExchangeOperationErrorNotification
| RefreshOperationErrorNotification
| RefundStatusOperationErrorNotification
| RefundApplyOperationErrorNotification
| ProposalOperationErrorNotification
| PayOperationErrorNotification
| TipOperationErrorNotification
| ProposalAcceptedNotification | ProposalAcceptedNotification
| ProposalDownloadedNotification | ProposalDownloadedNotification
| RefundsSubmittedNotification | RefundsSubmittedNotification
@ -288,22 +176,12 @@ export type WalletNotification =
| RefreshRevealedNotification | RefreshRevealedNotification
| RefreshStartedNotification | RefreshStartedNotification
| RefreshRefusedNotification | RefreshRefusedNotification
| ReserveCreatedNotification
| ReserveConfirmedNotification
| WithdrawalGroupFinishedNotification | WithdrawalGroupFinishedNotification
| RefundStartedNotification | RefundStartedNotification
| RefundQueriedNotification | RefundQueriedNotification
| WithdrawalGroupCreatedNotification | WithdrawalGroupCreatedNotification
| CoinWithdrawnNotification | CoinWithdrawnNotification
| RecoupOperationErrorNotification
| DepositOperationErrorNotification
| InternalErrorNotification | InternalErrorNotification
| PendingOperationProcessedNotification | PendingOperationProcessedNotification
| ProposalRefusedNotification
| ReserveRegisteredWithBankNotification
| ReserveNotYetFoundNotification
| PayOperationSuccessNotification
| KycRequestedNotification | KycRequestedNotification
| WithdrawalGroupBankConfirmed
| WithdrawalGroupReserveReadyNotification
| TransactionStateTransitionNotification; | TransactionStateTransitionNotification;

View File

@ -70,7 +70,7 @@ import {
StoreDescriptor, StoreDescriptor,
StoreWithIndexes, StoreWithIndexes,
} from "./util/query.js"; } 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 * This file contains the database schema of the Taler wallet together

View File

@ -35,6 +35,7 @@ import {
DenominationInfo, DenominationInfo,
RefreshGroupId, RefreshGroupId,
RefreshReason, RefreshReason,
TransactionState,
WalletNotification, WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; 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. * of wallet operations.
* *
* FIXME: This should not be exported anywhere from the taler-wallet-core package, * FIXME: This should not be exported anywhere from the taler-wallet-core package,
@ -183,6 +184,12 @@ export interface InternalWalletState {
merchantOps: MerchantOperations; merchantOps: MerchantOperations;
refreshOps: RefreshOperations; refreshOps: RefreshOperations;
getTransactionState(
ws: InternalWalletState,
tx: GetReadOnlyAccess<typeof WalletStoresV1>,
transactionId: string,
): Promise<TransactionState | undefined>;
getDenomInfo( getDenomInfo(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadOnlyAccess<{ tx: GetReadOnlyAccess<{

View File

@ -62,7 +62,7 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js"; import { assertUnreachable } from "../../util/assertUnreachable.js";
import { checkLogicInvariant } from "../../util/invariants.js"; import { checkLogicInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.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 { getExchangeDetails } from "../exchanges.js";
import { extractContractData } from "../pay-merchant.js"; import { extractContractData } from "../pay-merchant.js";
import { provideBackupState } from "./state.js"; import { provideBackupState } from "./state.js";
@ -472,7 +472,10 @@ export async function importBackup(
for (const backupWg of backupBlob.withdrawal_groups) { for (const backupWg of backupBlob.withdrawal_groups) {
const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv]; const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
checkLogicInvariant(!!reservePub); checkLogicInvariant(!!reservePub);
const ts = makeTombstoneId(TombstoneTag.DeleteReserve, reservePub); const ts = constructTombstone({
tag: TombstoneTag.DeleteReserve,
reservePub,
});
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
@ -558,10 +561,10 @@ export async function importBackup(
} }
for (const backupPurchase of backupBlob.purchases) { for (const backupPurchase of backupBlob.purchases) {
const ts = makeTombstoneId( const ts = constructTombstone({
TombstoneTag.DeletePayment, tag: TombstoneTag.DeletePayment,
backupPurchase.proposal_id, proposalId: backupPurchase.proposal_id,
); });
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
@ -704,10 +707,10 @@ export async function importBackup(
} }
for (const backupRefreshGroup of backupBlob.refresh_groups) { for (const backupRefreshGroup of backupBlob.refresh_groups) {
const ts = makeTombstoneId( const ts = constructTombstone({
TombstoneTag.DeleteRefreshGroup, tag: TombstoneTag.DeleteRefreshGroup,
backupRefreshGroup.refresh_group_id, refreshGroupId: backupRefreshGroup.refresh_group_id,
); });
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
@ -800,10 +803,10 @@ export async function importBackup(
} }
for (const backupTip of backupBlob.tips) { for (const backupTip of backupBlob.tips) {
const ts = makeTombstoneId( const ts = constructTombstone({
TombstoneTag.DeleteTip, tag: TombstoneTag.DeleteTip,
backupTip.wallet_tip_id, walletTipId: backupTip.wallet_tip_id,
); });
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }

View File

@ -29,52 +29,52 @@ import {
AmountString, AmountString,
AttentionType, AttentionType,
BackupRecovery, BackupRecovery,
Codec,
DenomKeyType,
EddsaKeyPair,
HttpStatusCode,
Logger,
PreparePayResult,
PreparePayResultType,
RecoveryLoadRequest,
RecoveryMergeStrategy,
TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
URL,
WalletBackupContentV1,
buildCodecForObject, buildCodecForObject,
buildCodecForUnion, buildCodecForUnion,
bytesToString, bytesToString,
canonicalizeBaseUrl,
canonicalJson, canonicalJson,
Codec, canonicalizeBaseUrl,
codecForAmountString, codecForAmountString,
codecForBoolean, codecForBoolean,
codecForConstString, codecForConstString,
codecForList, codecForList,
codecForNumber, codecForNumber,
codecForString, codecForString,
codecForTalerErrorDetail,
codecOptional, codecOptional,
ConfirmPayResultType,
decodeCrock, decodeCrock,
DenomKeyType,
durationFromSpec, durationFromSpec,
eddsaGetPublic, eddsaGetPublic,
EddsaKeyPair,
encodeCrock, encodeCrock,
getRandomBytes, getRandomBytes,
hash, hash,
hashDenomPub, hashDenomPub,
HttpStatusCode,
j2s, j2s,
kdf, kdf,
Logger,
notEmpty, notEmpty,
PaymentStatus,
PreparePayResult,
PreparePayResultType,
RecoveryLoadRequest,
RecoveryMergeStrategy,
ReserveTransactionType,
rsaBlind, rsaBlind,
secretbox, secretbox,
secretbox_open, secretbox_open,
stringToBytes, stringToBytes,
TalerErrorCode,
TalerErrorDetail,
TalerProtocolTimestamp,
TalerPreciseTimestamp,
URL,
WalletBackupContentV1,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { gunzipSync, gzipSync } from "fflate"; import { gunzipSync, gzipSync } from "fflate";
import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js"; import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js";
import { import {
@ -86,29 +86,19 @@ import {
ConfigRecordKey, ConfigRecordKey,
WalletBackupConfState, WalletBackupConfState,
} from "../../db.js"; } from "../../db.js";
import { TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js"; import { assertUnreachable } from "../../util/assertUnreachable.js";
import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { import {
checkDbInvariant, checkDbInvariant,
checkLogicInvariant, checkLogicInvariant,
} from "../../util/invariants.js"; } from "../../util/invariants.js";
import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
import { import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
TaskIdentifiers, TaskIdentifiers,
scheduleRetryInTx, } from "../common.js";
} from "../../util/retries.js"; import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
import {
checkPaymentByProposalId,
confirmPay,
preparePayForUri,
} from "../pay-merchant.js";
import { exportBackup } from "./export.js"; import { exportBackup } from "./export.js";
import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
import { getWalletBackupState, provideBackupState } from "./state.js"; import { getWalletBackupState, provideBackupState } from "./state.js";
@ -380,8 +370,6 @@ async function runBackupCycleForProvider(
logger.warn("backup provider not found anymore"); logger.warn("backup provider not found anymore");
return; return;
} }
const opId = TaskIdentifiers.forBackup(prov);
await scheduleRetryInTx(ws, tx, opId);
prov.shouldRetryFreshProposal = true; prov.shouldRetryFreshProposal = true;
prov.state = { prov.state = {
tag: BackupProviderStateTag.Retrying, tag: BackupProviderStateTag.Retrying,
@ -407,7 +395,7 @@ async function runBackupCycleForProvider(
return; return;
} }
const opId = TaskIdentifiers.forBackup(prov); const opId = TaskIdentifiers.forBackup(prov);
await scheduleRetryInTx(ws, tx, opId); //await scheduleRetryInTx(ws, tx, opId);
prov.currentPaymentProposalId = result.proposalId; prov.currentPaymentProposalId = result.proposalId;
prov.shouldRetryFreshProposal = false; prov.shouldRetryFreshProposal = false;
prov.state = { prov.state = {
@ -481,7 +469,7 @@ async function runBackupCycleForProvider(
// FIXME: Allocate error code for this situation? // FIXME: Allocate error code for this situation?
// FIXME: Add operation retry record! // FIXME: Add operation retry record!
const opId = TaskIdentifiers.forBackup(prov); const opId = TaskIdentifiers.forBackup(prov);
await scheduleRetryInTx(ws, tx, opId); //await scheduleRetryInTx(ws, tx, opId);
prov.state = { prov.state = {
tag: BackupProviderStateTag.Retrying, tag: BackupProviderStateTag.Retrying,
}; };

View File

@ -18,42 +18,56 @@
* Imports. * Imports.
*/ */
import { import {
AbsoluteTime,
AgeRestriction, AgeRestriction,
AmountJson, AmountJson,
Amounts, Amounts,
CancellationToken, CancellationToken,
CoinRefreshRequest, CoinRefreshRequest,
CoinStatus, CoinStatus,
Duration,
ErrorInfoSummary,
ExchangeEntryStatus, ExchangeEntryStatus,
ExchangeListItem, ExchangeListItem,
ExchangeTosStatus, ExchangeTosStatus,
getErrorDetailFromException, getErrorDetailFromException,
j2s, j2s,
Logger, Logger,
NotificationType,
OperationErrorInfo, OperationErrorInfo,
RefreshReason, RefreshReason,
TalerErrorCode, TalerErrorCode,
TalerErrorDetail, TalerErrorDetail,
TombstoneIdStr, TombstoneIdStr,
TransactionIdStr, TransactionIdStr,
TransactionType,
WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
WalletStoresV1, WalletStoresV1,
CoinRecord, CoinRecord,
ExchangeDetailsRecord, ExchangeDetailsRecord,
ExchangeRecord, ExchangeRecord,
BackupProviderRecord,
DepositGroupRecord,
PeerPullPaymentIncomingRecord,
PeerPullPaymentInitiationRecord,
PeerPushPaymentIncomingRecord,
PeerPushPaymentInitiationRecord,
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
TipRecord,
WithdrawalGroupRecord,
} from "../db.js"; } from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { GetReadWriteAccess } from "../util/query.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
RetryInfo,
} from "../util/retries.js";
import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.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"); 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, ws: InternalWalletState,
pendingTaskId: string, pendingTaskId: string,
e: TalerErrorDetail, e: TalerErrorDetail,
): Promise<void> { ): Promise<void> {
await ws.db logger.info(`storing pending task error for ${pendingTaskId}`);
.mktx((x) => [x.operationRetries]) const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
.runReadWrite(async (tx) => { let retryRecord = await tx.operationRetries.get(pendingTaskId);
let retryRecord = await tx.operationRetries.get(pendingTaskId); if (!retryRecord) {
if (!retryRecord) { retryRecord = {
retryRecord = { id: pendingTaskId,
id: pendingTaskId, lastError: e,
lastError: e, retryInfo: RetryInfo.reset(),
retryInfo: RetryInfo.reset(), };
}; } else {
} else { retryRecord.lastError = e;
retryRecord.lastError = e; retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo); }
} await tx.operationRetries.put(retryRecord);
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<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);
}
}
async function storePendingTaskPending(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
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, ws: InternalWalletState,
pendingTaskId: string, pendingTaskId: string,
): Promise<void> { ): Promise<void> {
await ws.db await ws.db
.mktx((x) => [x.operationRetries]) .mktx((x) => [x.operationRetries])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId); await tx.operationRetries.delete(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);
}
}); });
} }
export async function storeOperationPending( export async function runTaskWithErrorReporting<T1, T2>(
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);
});
}
export async function runOperationWithErrorReporting<T1, T2>(
ws: InternalWalletState, ws: InternalWalletState,
opId: TaskId, opId: TaskId,
f: () => Promise<OperationAttemptResult<T1, T2>>, f: () => Promise<OperationAttemptResult<T1, T2>>,
@ -268,13 +399,13 @@ export async function runOperationWithErrorReporting<T1, T2>(
const resp = await f(); const resp = await f();
switch (resp.type) { switch (resp.type) {
case OperationAttemptResultType.Error: case OperationAttemptResultType.Error:
await storeOperationError(ws, opId, resp.errorDetail); await storePendingTaskError(ws, opId, resp.errorDetail);
return resp; return resp;
case OperationAttemptResultType.Finished: case OperationAttemptResultType.Finished:
await storeOperationFinished(ws, opId); await storePendingTaskFinished(ws, opId);
return resp; return resp;
case OperationAttemptResultType.Pending: case OperationAttemptResultType.Pending:
await storeOperationPending(ws, opId); await storePendingTaskPending(ws, opId);
return resp; return resp;
case OperationAttemptResultType.Longpoll: case OperationAttemptResultType.Longpoll:
return resp; return resp;
@ -297,7 +428,7 @@ export async function runOperationWithErrorReporting<T1, T2>(
logger.warn("operation processed resulted in error"); logger.warn("operation processed resulted in error");
logger.warn(`error was: ${j2s(e.errorDetail)}`); logger.warn(`error was: ${j2s(e.errorDetail)}`);
maybeError = e.errorDetail; maybeError = e.errorDetail;
await storeOperationError(ws, opId, maybeError!); await storePendingTaskError(ws, opId, maybeError!);
return { return {
type: OperationAttemptResultType.Error, type: OperationAttemptResultType.Error,
errorDetail: e.errorDetail, errorDetail: e.errorDetail,
@ -315,7 +446,7 @@ export async function runOperationWithErrorReporting<T1, T2>(
}, },
`unexpected exception (message: ${e.message})`, `unexpected exception (message: ${e.message})`,
); );
await storeOperationError(ws, opId, maybeError); await storePendingTaskError(ws, opId, maybeError);
return { return {
type: OperationAttemptResultType.Error, type: OperationAttemptResultType.Error,
errorDetail: maybeError, errorDetail: maybeError,
@ -327,7 +458,7 @@ export async function runOperationWithErrorReporting<T1, T2>(
{}, {},
`unexpected exception (not even an error)`, `unexpected exception (not even an error)`,
); );
await storeOperationError(ws, opId, maybeError); await storePendingTaskError(ws, opId, maybeError);
return { return {
type: OperationAttemptResultType.Error, type: OperationAttemptResultType.Error,
errorDetail: maybeError, 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 { export enum TombstoneTag {
DeleteWithdrawalGroup = "delete-withdrawal-group", DeleteWithdrawalGroup = "delete-withdrawal-group",
DeleteReserve = "delete-reserve", DeleteReserve = "delete-reserve",
@ -361,15 +481,6 @@ export enum TombstoneTag {
DeletePeerPushCredit = "delete-peer-push-credit", 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( export function getExchangeTosStatus(
exchangeDetails: ExchangeDetailsRecord, exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus { ): ExchangeTosStatus {
@ -432,7 +543,7 @@ export function runLongpollAsync(
const asyncFn = async () => { const asyncFn = async () => {
if (ws.stopped) { if (ws.stopped) {
logger.trace("not long-polling reserve, wallet already stopped"); logger.trace("not long-polling reserve, wallet already stopped");
await storeOperationPending(ws, retryTag); await storePendingTaskPending(ws, retryTag);
return; return;
} }
const cts = CancellationToken.create(); const cts = CancellationToken.create();
@ -446,13 +557,13 @@ export function runLongpollAsync(
}; };
res = await reqFn(cts.token); res = await reqFn(cts.token);
} catch (e) { } catch (e) {
await storeOperationError(ws, retryTag, getErrorDetailFromException(e)); await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e));
return; return;
} finally { } finally {
delete ws.activeLongpoll[retryTag]; delete ws.activeLongpoll[retryTag];
} }
if (!res.ready) { if (!res.ready) {
await storeOperationPending(ws, retryTag); await storePendingTaskPending(ws, retryTag);
} }
ws.workAvailable.trigger(); ws.workAvailable.trigger();
}; };
@ -464,7 +575,11 @@ export type ParsedTombstone =
tag: TombstoneTag.DeleteWithdrawalGroup; tag: TombstoneTag.DeleteWithdrawalGroup;
withdrawalGroupId: string; 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 { export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
switch (p.tag) { switch (p.tag) {
@ -472,6 +587,16 @@ export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteRefund: case TombstoneTag.DeleteRefund:
return `tmb:${p.tag}:${p.refundGroupId}` as TombstoneIdStr; 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>; resume(): Promise<void>;
process(): Promise<OperationAttemptResult>; 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})`);
}
}

View File

@ -79,8 +79,7 @@ import {
} from "../index.js"; } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { OperationAttemptResult } from "../util/retries.js"; import { constructTaskIdentifier, OperationAttemptResult, spendCoins, TombstoneTag } from "./common.js";
import { spendCoins, TombstoneTag } from "./common.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { import {
extractContractData, extractContractData,
@ -94,7 +93,6 @@ import {
parseTransactionIdentifier, parseTransactionIdentifier,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { constructTaskIdentifier } from "../util/retries.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
/** /**

View File

@ -74,14 +74,8 @@ import {
GetReadOnlyAccess, GetReadOnlyAccess,
GetReadWriteAccess, GetReadWriteAccess,
} from "../util/query.js"; } from "../util/query.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
unwrapOperationHandlerResultOrThrow,
} from "../util/retries.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.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"); const logger = new Logger("exchanges.ts");
@ -559,7 +553,7 @@ export async function updateExchangeFromUrl(
}> { }> {
const canonUrl = canonicalizeBaseUrl(baseUrl); const canonUrl = canonicalizeBaseUrl(baseUrl);
return unwrapOperationHandlerResultOrThrow( return unwrapOperationHandlerResultOrThrow(
await runOperationWithErrorReporting( await runTaskWithErrorReporting(
ws, ws,
TaskIdentifiers.forExchangeUpdateFromUrl(canonUrl), TaskIdentifiers.forExchangeUpdateFromUrl(canonUrl),
() => updateExchangeFromUrlHandler(ws, canonUrl, options), () => updateExchangeFromUrlHandler(ws, canonUrl, options),

View File

@ -67,7 +67,6 @@ import {
TalerErrorCode, TalerErrorCode,
TalerErrorDetail, TalerErrorDetail,
TalerPreciseTimestamp, TalerPreciseTimestamp,
TalerProtocolTimestamp,
TalerProtocolViolationError, TalerProtocolViolationError,
TalerUriAction, TalerUriAction,
TransactionAction, TransactionAction,
@ -116,12 +115,11 @@ import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
RetryInfo, RetryInfo,
scheduleRetry,
TaskIdentifiers, TaskIdentifiers,
} from "../util/retries.js"; } from "./common.js";
import { import {
runLongpollAsync, runLongpollAsync,
runOperationWithErrorReporting, runTaskWithErrorReporting,
spendCoins, spendCoins,
} from "./common.js"; } from "./common.js";
import { import {
@ -1254,7 +1252,7 @@ export async function runPayForConfirmPay(
tag: PendingTaskType.Purchase, tag: PendingTaskType.Purchase,
proposalId, proposalId,
}); });
const res = await runOperationWithErrorReporting(ws, taskId, async () => { const res = await runTaskWithErrorReporting(ws, taskId, async () => {
return await processPurchasePay(ws, proposalId, { forceNow: true }); return await processPurchasePay(ws, proposalId, { forceNow: true });
}); });
logger.trace(`processPurchasePay response type ${res.type}`); 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 // Do this in the background, as it might take some time
handleInsufficientFunds(ws, proposalId, err).catch(async (e) => { handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
console.log("handling insufficient funds failed"); console.log("handling insufficient funds failed");
console.log(`${e.toString()}`);
await scheduleRetry(ws, TaskIdentifiers.forPay(purchase), {
code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
when: AbsoluteTime.now(),
message: "unexpected exception",
hint: "unexpected exception",
details: {
exception: e.toString(),
},
});
}); });
// FIXME: Should we really consider this to be pending?
return { return {
type: OperationAttemptResultType.Pending, type: OperationAttemptResultType.Pending,
result: undefined, result: undefined,
@ -1694,11 +1685,6 @@ export async function processPurchasePay(
await unblockBackup(ws, proposalId); await unblockBackup(ws, proposalId);
} }
ws.notify({
type: NotificationType.PayOperationSuccess,
proposalId: purchase.proposalId,
});
return OperationAttemptResult.finishedEmpty(); return OperationAttemptResult.finishedEmpty();
} }

View File

@ -52,11 +52,6 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { getTotalRefreshCost } from "./refresh.js"; import { getTotalRefreshCost } from "./refresh.js";
import {
OperationAttemptLongpollResult,
OperationAttemptResult,
OperationAttemptResultType,
} from "../util/retries.js";
const logger = new Logger("operations/peer-to-peer.ts"); const logger = new Logger("operations/peer-to-peer.ts");

View File

@ -66,12 +66,9 @@ import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
constructTaskIdentifier, constructTaskIdentifier,
} from "../util/retries.js";
import {
LongpollResult, LongpollResult,
resetOperationTimeout,
runLongpollAsync, runLongpollAsync,
runOperationWithErrorReporting, runTaskWithErrorReporting,
} from "./common.js"; } from "./common.js";
import { import {
codecForExchangePurseStatus, codecForExchangePurseStatus,
@ -486,26 +483,6 @@ export async function processPeerPullCredit(
switch (pullIni.status) { switch (pullIni.status) {
case PeerPullPaymentInitiationStatus.Done: { 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 { return {
type: OperationAttemptResultType.Finished, type: OperationAttemptResultType.Finished,
result: undefined, result: undefined,
@ -811,7 +788,7 @@ export async function initiatePeerPullPayment(
pursePub: pursePair.pub, pursePub: pursePair.pub,
}); });
await runOperationWithErrorReporting(ws, taskId, async () => { await runTaskWithErrorReporting(ws, taskId, async () => {
return processPeerPullCredit(ws, pursePair.pub); return processPeerPullCredit(ws, pursePair.pub);
}); });

View File

@ -59,13 +59,15 @@ import {
createRefreshGroup, createRefreshGroup,
} from "../index.js"; } from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
TaskIdentifiers, TaskIdentifiers,
constructTaskIdentifier, constructTaskIdentifier,
} from "../util/retries.js"; runTaskWithErrorReporting,
import { runOperationWithErrorReporting, spendCoins } from "./common.js"; spendCoins,
} from "./common.js";
import { import {
PeerCoinRepair, PeerCoinRepair,
codecForExchangePurseStatus, codecForExchangePurseStatus,
@ -78,7 +80,6 @@ import {
notifyTransition, notifyTransition,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { checkLogicInvariant } from "../util/invariants.js";
const logger = new Logger("pay-peer-pull-debit.ts"); const logger = new Logger("pay-peer-pull-debit.ts");
@ -462,7 +463,7 @@ export async function confirmPeerPullDebit(
return pi; return pi;
}); });
await runOperationWithErrorReporting( await runTaskWithErrorReporting(
ws, ws,
TaskIdentifiers.forPeerPullPaymentDebit(ppi), TaskIdentifiers.forPeerPullPaymentDebit(ppi),
async () => { async () => {

View File

@ -60,12 +60,7 @@ import {
} from "../index.js"; } from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
import { import { OperationAttemptResult, OperationAttemptResultType, constructTaskIdentifier, runLongpollAsync } from "./common.js";
OperationAttemptResult,
OperationAttemptResultType,
constructTaskIdentifier,
} from "../util/retries.js";
import { runLongpollAsync } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js"; import { updateExchangeFromUrl } from "./exchanges.js";
import { import {
codecForExchangePurseStatus, codecForExchangePurseStatus,

View File

@ -42,40 +42,41 @@ import {
j2s, j2s,
stringifyTalerUri, stringifyTalerUri,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import {
selectPeerCoins,
getTotalPeerPaymentCost,
codecForExchangePurseStatus,
queryCoinInfosForSelection,
PeerCoinRepair,
} from "./pay-peer-common.js";
import { import {
HttpResponse, HttpResponse,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
readTalerErrorResponse, readTalerErrorResponse,
} from "@gnu-taler/taler-util/http"; } from "@gnu-taler/taler-util/http";
import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
import { import {
PeerPushPaymentInitiationRecord, PeerPushPaymentInitiationRecord,
PeerPushPaymentInitiationStatus, PeerPushPaymentInitiationStatus,
RefreshOperationStatus, RefreshOperationStatus,
createRefreshGroup, createRefreshGroup,
} from "../index.js"; } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js"; import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
constructTaskIdentifier, constructTaskIdentifier,
} from "../util/retries.js"; runLongpollAsync,
import { runLongpollAsync, spendCoins } from "./common.js"; spendCoins,
} from "./common.js";
import {
PeerCoinRepair,
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
queryCoinInfosForSelection,
selectPeerCoins,
} from "./pay-peer-common.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
notifyTransition, notifyTransition,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } 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"); const logger = new Logger("pay-peer-push-debit.ts");
@ -162,10 +163,10 @@ async function handlePurseCreationConflict(
case PeerPushPaymentInitiationStatus.PendingCreatePurse: case PeerPushPaymentInitiationStatus.PendingCreatePurse:
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: { case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: {
const sel = coinSelRes.result; const sel = coinSelRes.result;
myPpi.coinSel = { myPpi.coinSel = {
coinPubs: sel.coins.map((x) => x.coinPub), coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution), contributions: sel.coins.map((x) => x.contribution),
} };
break; break;
} }
default: default:

View File

@ -43,8 +43,8 @@ import {
import { AbsoluteTime } from "@gnu-taler/taler-util"; import { AbsoluteTime } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { GetReadOnlyAccess } from "../util/query.js"; import { GetReadOnlyAccess } from "../util/query.js";
import { TaskIdentifiers } from "../util/retries.js";
import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { GlobalIDB } from "@gnu-taler/idb-bridge";
import { TaskIdentifiers } from "./common.js";
function getPendingCommon( function getPendingCommon(
ws: InternalWalletState, ws: InternalWalletState,

View File

@ -53,12 +53,9 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
import { GetReadWriteAccess } from "../util/query.js"; import { GetReadWriteAccess } from "../util/query.js";
import {
OperationAttemptResult,
unwrapOperationHandlerResultOrThrow,
} from "../util/retries.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh.js"; import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js";
import { OperationAttemptResult } from "./common.js";
const logger = new Logger("operations/recoup.ts"); const logger = new Logger("operations/recoup.ts");

View File

@ -84,18 +84,12 @@ import {
} from "@gnu-taler/taler-util/http"; } from "@gnu-taler/taler-util/http";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import { import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
constructTaskIdentifier,
OperationAttemptResult,
OperationAttemptResultType,
} from "../util/retries.js";
import { makeCoinAvailable } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js"; import { updateExchangeFromUrl } from "./exchanges.js";
import { selectWithdrawalDenominations } from "../util/coinSelection.js"; import { selectWithdrawalDenominations } from "../util/coinSelection.js";
import { import {
isWithdrawableDenom, isWithdrawableDenom,
PendingTaskType, PendingTaskType,
WalletConfig,
} from "../index.js"; } from "../index.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,

View File

@ -57,12 +57,7 @@ import {
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http"; } from "@gnu-taler/taler-util/http";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, OperationAttemptResultType } from "./common.js";
constructTaskIdentifier,
OperationAttemptResult,
OperationAttemptResultType,
} from "../util/retries.js";
import { makeCoinAvailable } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js"; import { updateExchangeFromUrl } from "./exchanges.js";
import { import {
getCandidateWithdrawalDenoms, getCandidateWithdrawalDenoms,

View File

@ -69,8 +69,12 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js"; import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { constructTaskIdentifier, TaskIdentifiers } from "../util/retries.js"; import {
import { resetOperationTimeout, TombstoneTag } from "./common.js"; constructTaskIdentifier,
resetPendingTaskTimeout,
TaskIdentifiers,
TombstoneTag,
} from "./common.js";
import { import {
abortDepositGroup, abortDepositGroup,
failDepositTransaction, failDepositTransaction,
@ -1388,7 +1392,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPullCredit, tag: PendingTaskType.PeerPullCredit,
pursePub: parsedTx.pursePub, pursePub: parsedTx.pursePub,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1397,7 +1401,7 @@ export async function retryTransaction(
tag: PendingTaskType.Deposit, tag: PendingTaskType.Deposit,
depositGroupId: parsedTx.depositGroupId, depositGroupId: parsedTx.depositGroupId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1408,7 +1412,7 @@ export async function retryTransaction(
tag: PendingTaskType.Withdraw, tag: PendingTaskType.Withdraw,
withdrawalGroupId: parsedTx.withdrawalGroupId, withdrawalGroupId: parsedTx.withdrawalGroupId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1417,7 +1421,7 @@ export async function retryTransaction(
tag: PendingTaskType.Purchase, tag: PendingTaskType.Purchase,
proposalId: parsedTx.proposalId, proposalId: parsedTx.proposalId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1426,7 +1430,7 @@ export async function retryTransaction(
tag: PendingTaskType.TipPickup, tag: PendingTaskType.TipPickup,
walletTipId: parsedTx.walletTipId, walletTipId: parsedTx.walletTipId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1435,7 +1439,7 @@ export async function retryTransaction(
tag: PendingTaskType.Refresh, tag: PendingTaskType.Refresh,
refreshGroupId: parsedTx.refreshGroupId, refreshGroupId: parsedTx.refreshGroupId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1444,7 +1448,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPullDebit, tag: PendingTaskType.PeerPullDebit,
peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId, peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1453,7 +1457,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPushCredit, tag: PendingTaskType.PeerPushCredit,
peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId, peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
@ -1462,7 +1466,7 @@ export async function retryTransaction(
tag: PendingTaskType.PeerPushDebit, tag: PendingTaskType.PeerPushDebit,
pursePub: parsedTx.pursePub, pursePub: parsedTx.pursePub,
}); });
await resetOperationTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }

View File

@ -92,10 +92,13 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
constructTaskIdentifier,
makeCoinAvailable, makeCoinAvailable,
makeExchangeListItem, makeExchangeListItem,
runLongpollAsync, runLongpollAsync,
runOperationWithErrorReporting,
} from "../operations/common.js"; } from "../operations/common.js";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
@ -114,12 +117,6 @@ import {
GetReadOnlyAccess, GetReadOnlyAccess,
GetReadWriteAccess, GetReadWriteAccess,
} from "../util/query.js"; } from "../util/query.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
constructTaskIdentifier,
} from "../util/retries.js";
import { import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION,
@ -1225,10 +1222,6 @@ async function queryReserve(
result.talerErrorResponse.code === result.talerErrorResponse.code ===
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) { ) {
ws.notify({
type: NotificationType.ReserveNotYetFound,
reservePub,
});
return { ready: false }; return { ready: false };
} else { } else {
throwUnexpectedRequestError(resp, result.talerErrorResponse); throwUnexpectedRequestError(resp, result.talerErrorResponse);
@ -1258,12 +1251,6 @@ async function queryReserve(
notifyTransition(ws, transactionId, transitionResult); notifyTransition(ws, transactionId, transitionResult);
// FIXME: This notification is deprecated with DD37
ws.notify({
type: NotificationType.WithdrawalGroupReserveReady,
transactionId,
});
return { ready: true }; return { ready: true };
} }
@ -2053,8 +2040,6 @@ async function registerReserveWithBank(
}); });
notifyTransition(ws, transactionId, transitionInfo); notifyTransition(ws, transactionId, transitionInfo);
// FIXME: This notification is deprecated with DD37
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
} }
interface BankStatusResult { interface BankStatusResult {
@ -2176,15 +2161,6 @@ async function processReserveBankStatus(
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now; r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.PendingQueryingStatus; 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 { } else {
logger.info("withdrawal: transfer not yet confirmed by bank"); logger.info("withdrawal: transfer not yet confirmed by bank");
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url; r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;

View File

@ -25,7 +25,7 @@
* Imports. * Imports.
*/ */
import { TalerErrorDetail, AbsoluteTime } from "@gnu-taler/taler-util"; import { TalerErrorDetail, AbsoluteTime } from "@gnu-taler/taler-util";
import { RetryInfo } from "./util/retries.js"; import { RetryInfo } from "./operations/common.js";
export enum PendingTaskType { export enum PendingTaskType {
ExchangeUpdate = "exchange-update", ExchangeUpdate = "exchange-update",

View File

@ -50,309 +50,3 @@ import { assertUnreachable } from "./assertUnreachable.js";
const logger = new Logger("util/retries.ts"); 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})`);
}
}

View File

@ -48,6 +48,7 @@ import {
RefreshReason, RefreshReason,
TalerError, TalerError,
TalerErrorCode, TalerErrorCode,
TransactionState,
TransactionType, TransactionType,
URL, URL,
ValidateIbanResponse, ValidateIbanResponse,
@ -170,9 +171,10 @@ import { getBalanceDetail, getBalances } from "./operations/balance.js";
import { import {
getExchangeTosStatus, getExchangeTosStatus,
makeExchangeListItem, makeExchangeListItem,
runOperationWithErrorReporting, runTaskWithErrorReporting,
} from "./operations/common.js"; } from "./operations/common.js";
import { import {
computeDepositTransactionStatus,
createDepositGroup, createDepositGroup,
generateDepositGroupTxId, generateDepositGroupTxId,
prepareDepositGroup, prepareDepositGroup,
@ -191,6 +193,9 @@ import {
} from "./operations/exchanges.js"; } from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js"; import { getMerchantInfo } from "./operations/merchants.js";
import { import {
computePayMerchantTransactionActions,
computePayMerchantTransactionState,
computeRefundTransactionState,
confirmPay, confirmPay,
getContractTermsDetails, getContractTermsDetails,
preparePayForUri, preparePayForUri,
@ -200,21 +205,25 @@ import {
} from "./operations/pay-merchant.js"; } from "./operations/pay-merchant.js";
import { import {
checkPeerPullPaymentInitiation, checkPeerPullPaymentInitiation,
computePeerPullCreditTransactionState,
initiatePeerPullPayment, initiatePeerPullPayment,
processPeerPullCredit, processPeerPullCredit,
} from "./operations/pay-peer-pull-credit.js"; } from "./operations/pay-peer-pull-credit.js";
import { import {
computePeerPullDebitTransactionState,
confirmPeerPullDebit, confirmPeerPullDebit,
preparePeerPullDebit, preparePeerPullDebit,
processPeerPullDebit, processPeerPullDebit,
} from "./operations/pay-peer-pull-debit.js"; } from "./operations/pay-peer-pull-debit.js";
import { import {
computePeerPushCreditTransactionState,
confirmPeerPushCredit, confirmPeerPushCredit,
preparePeerPushCredit, preparePeerPushCredit,
processPeerPushCredit, processPeerPushCredit,
} from "./operations/pay-peer-push-credit.js"; } from "./operations/pay-peer-push-credit.js";
import { import {
checkPeerPushDebit, checkPeerPushDebit,
computePeerPushDebitTransactionState,
initiatePeerPushDebit, initiatePeerPushDebit,
processPeerPushDebit, processPeerPushDebit,
} from "./operations/pay-peer-push-debit.js"; } 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 { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
import { import {
autoRefresh, autoRefresh,
computeRefreshTransactionState,
createRefreshGroup, createRefreshGroup,
processRefreshGroup, processRefreshGroup,
} from "./operations/refresh.js"; } from "./operations/refresh.js";
@ -231,7 +241,7 @@ import {
testPay, testPay,
withdrawTestBalance, withdrawTestBalance,
} from "./operations/testing.js"; } from "./operations/testing.js";
import { acceptTip, prepareTip, processTip } from "./operations/tip.js"; import { acceptTip, computeTipTransactionStatus, prepareTip, processTip } from "./operations/tip.js";
import { import {
abortTransaction, abortTransaction,
deleteTransaction, deleteTransaction,
@ -245,6 +255,7 @@ import {
} from "./operations/transactions.js"; } from "./operations/transactions.js";
import { import {
acceptWithdrawalFromUri, acceptWithdrawalFromUri,
computeWithdrawalTransactionStatus,
createManualWithdrawal, createManualWithdrawal,
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri, getWithdrawalDetailsForUri,
@ -268,7 +279,7 @@ import {
GetReadOnlyAccess, GetReadOnlyAccess,
GetReadWriteAccess, GetReadWriteAccess,
} from "./util/query.js"; } 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 { TimerAPI, TimerGroup } from "./util/timer.js";
import { import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@ -337,7 +348,7 @@ export async function runPending(ws: InternalWalletState): Promise<void> {
if (!AbsoluteTime.isExpired(p.timestampDue)) { if (!AbsoluteTime.isExpired(p.timestampDue)) {
continue; continue;
} }
await runOperationWithErrorReporting(ws, p.id, async () => { await runTaskWithErrorReporting(ws, p.id, async () => {
logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`); logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
return await callOperationHandler(ws, p); return await callOperationHandler(ws, p);
}); });
@ -439,7 +450,7 @@ async function runTaskLoop(
if (!AbsoluteTime.isExpired(p.timestampDue)) { if (!AbsoluteTime.isExpired(p.timestampDue)) {
continue; continue;
} }
await runOperationWithErrorReporting(ws, p.id, async () => { await runTaskWithErrorReporting(ws, p.id, async () => {
logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`); logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
return await callOperationHandler(ws, p); 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( async getDenomInfo(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadWriteAccess<{ tx: GetReadWriteAccess<{