wallet-core: emit DD37 self-transition notifications with errors
This commit is contained in:
parent
54f0c82999
commit
9c708251f9
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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<{
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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),
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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<{
|
||||||
|
Loading…
Reference in New Issue
Block a user