wallet-core/packages/taler-wallet-core/src/operations/common.ts

977 lines
30 KiB
TypeScript

/*
This file is part of GNU Taler
(C) 2022 GNUnet e.V.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
AbsoluteTime,
AgeRestriction,
AmountJson,
Amounts,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
Duration,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
ExchangeUpdateStatus,
getErrorDetailFromException,
j2s,
Logger,
makeErrorDetail,
NotificationType,
OperationErrorInfo,
RefreshReason,
TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TombstoneIdStr,
TransactionIdStr,
TransactionType,
WalletNotification,
} from "@gnu-taler/taler-util";
import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js";
import {
BackupProviderRecord,
CoinRecord,
DbPreciseTimestamp,
DepositGroupRecord,
ExchangeDetailsRecord,
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
PeerPullCreditRecord,
PeerPullPaymentIncomingRecord,
PeerPushDebitRecord,
PeerPushPaymentIncomingRecord,
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
RewardRecord,
timestampPreciseToDb,
WalletStoresV1,
WithdrawalGroupRecord,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType, TaskId } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import { constructTransactionIdentifier } from "./transactions.js";
const logger = new Logger("operations/common.ts");
export interface CoinsSpendInfo {
coinPubs: string[];
contributions: AmountJson[];
refreshReason: RefreshReason;
/**
* Identifier for what the coin has been spent for.
*/
allocationId: TransactionIdStr;
}
export async function makeCoinsVisible(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
coinAvailability: typeof WalletStoresV1.coinAvailability;
}>,
transactionId: string,
): Promise<void> {
const coins = await tx.coins.indexes.bySourceTransactionId.getAll(
transactionId,
);
for (const coinRecord of coins) {
if (!coinRecord.visible) {
coinRecord.visible = 1;
await tx.coins.put(coinRecord);
const ageRestriction = coinRecord.maxAge;
const car = await tx.coinAvailability.get([
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
ageRestriction,
]);
if (!car) {
logger.error("missing coin availability record");
continue;
}
const visCount = car.visibleCoinCount ?? 0;
car.visibleCoinCount = visCount + 1;
await tx.coinAvailability.put(car);
}
}
}
export async function makeCoinAvailable(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
coinAvailability: typeof WalletStoresV1.coinAvailability;
denominations: typeof WalletStoresV1.denominations;
}>,
coinRecord: CoinRecord,
): Promise<void> {
checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
const existingCoin = await tx.coins.get(coinRecord.coinPub);
if (existingCoin) {
return;
}
const denom = await tx.denominations.get([
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
]);
checkDbInvariant(!!denom);
const ageRestriction = coinRecord.maxAge;
let car = await tx.coinAvailability.get([
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
ageRestriction,
]);
if (!car) {
car = {
maxAge: ageRestriction,
value: denom.value,
currency: denom.currency,
denomPubHash: denom.denomPubHash,
exchangeBaseUrl: denom.exchangeBaseUrl,
freshCoinCount: 0,
visibleCoinCount: 0,
};
}
car.freshCoinCount++;
await tx.coins.put(coinRecord);
await tx.coinAvailability.put(car);
}
export async function spendCoins(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
coinAvailability: typeof WalletStoresV1.coinAvailability;
refreshGroups: typeof WalletStoresV1.refreshGroups;
denominations: typeof WalletStoresV1.denominations;
}>,
csi: CoinsSpendInfo,
): Promise<void> {
if (csi.coinPubs.length != csi.contributions.length) {
throw Error("assertion failed");
}
if (csi.coinPubs.length === 0) {
return;
}
let refreshCoinPubs: CoinRefreshRequest[] = [];
for (let i = 0; i < csi.coinPubs.length; i++) {
const coin = await tx.coins.get(csi.coinPubs[i]);
if (!coin) {
throw Error("coin allocated for payment doesn't exist anymore");
}
const denom = await ws.getDenomInfo(
ws,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
);
checkDbInvariant(!!denom);
const coinAvailability = await tx.coinAvailability.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
coin.maxAge,
]);
checkDbInvariant(!!coinAvailability);
const contrib = csi.contributions[i];
if (coin.status !== CoinStatus.Fresh) {
const alloc = coin.spendAllocation;
if (!alloc) {
continue;
}
if (alloc.id !== csi.allocationId) {
// FIXME: assign error code
logger.info("conflicting coin allocation ID");
logger.info(`old ID: ${alloc.id}, new ID: ${csi.allocationId}`);
throw Error("conflicting coin allocation (id)");
}
if (0 !== Amounts.cmp(alloc.amount, contrib)) {
// FIXME: assign error code
throw Error("conflicting coin allocation (contrib)");
}
continue;
}
coin.status = CoinStatus.Dormant;
coin.spendAllocation = {
id: csi.allocationId,
amount: Amounts.stringify(contrib),
};
const remaining = Amounts.sub(denom.value, contrib);
if (remaining.saturated) {
throw Error("not enough remaining balance on coin for payment");
}
refreshCoinPubs.push({
amount: Amounts.stringify(remaining.amount),
coinPub: coin.coinPub,
});
checkDbInvariant(!!coinAvailability);
if (coinAvailability.freshCoinCount === 0) {
throw Error(
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
);
}
coinAvailability.freshCoinCount--;
if (coin.visible) {
if (!coinAvailability.visibleCoinCount) {
logger.error("coin availability inconsistent");
} else {
coinAvailability.visibleCoinCount--;
}
}
await tx.coins.put(coin);
await tx.coinAvailability.put(coinAvailability);
}
await ws.refreshOps.createRefreshGroup(
ws,
tx,
Amounts.currencyOf(csi.contributions[0]),
refreshCoinPubs,
csi.refreshReason,
{
originatingTransactionId: csi.allocationId,
},
);
}
/**
* 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,
peerPullDebitId: parsedTaskId.peerPullDebitId,
});
// 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,
peerPushCreditId: parsedTaskId.peerPushCreditId,
});
case PendingTaskType.Deposit:
return constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId: parsedTaskId.depositGroupId,
});
case PendingTaskType.Refresh:
return constructTransactionIdentifier({
tag: TransactionType.Refresh,
refreshGroupId: parsedTaskId.refreshGroupId,
});
case PendingTaskType.RewardPickup:
return constructTransactionIdentifier({
tag: TransactionType.Reward,
walletRewardId: parsedTaskId.walletRewardId,
});
case PendingTaskType.PeerPushDebit:
return constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: parsedTaskId.pursePub,
});
case PendingTaskType.Purchase:
return constructTransactionIdentifier({
tag: TransactionType.Payment,
proposalId: parsedTaskId.proposalId,
});
default:
return undefined;
}
}
/**
* For tasks that process a transaction,
* generate a state transition notification.
*/
async function taskToTransactionNotification(
ws: InternalWalletState,
tx: GetReadOnlyAccess<typeof WalletStoresV1>,
pendingTaskId: string,
e: TalerErrorDetail | undefined,
): Promise<WalletNotification | undefined> {
const txId = convertTaskToTransactionId(pendingTaskId);
if (!txId) {
return undefined;
}
const txState = await ws.getTransactionState(ws, tx, txId);
if (!txState) {
return undefined;
}
const notif: WalletNotification = {
type: NotificationType.TransactionStateTransition,
transactionId: txId,
oldTxState: txState,
newTxState: txState,
};
if (e) {
notif.errorInfo = {
code: e.code as number,
hint: e.hint,
};
}
return notif;
}
async function storePendingTaskError(
ws: InternalWalletState,
pendingTaskId: string,
e: TalerErrorDetail,
): Promise<void> {
logger.info(`storing pending task error for ${pendingTaskId}`);
const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
if (!retryRecord) {
retryRecord = {
id: pendingTaskId,
lastError: e,
retryInfo: DbRetryInfo.reset(),
};
} else {
retryRecord.lastError = e;
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
await tx.operationRetries.put(retryRecord);
return taskToTransactionNotification(ws, tx, pendingTaskId, e);
});
if (maybeNotification) {
ws.notify(maybeNotification);
}
}
export async function resetPendingTaskTimeout(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
let retryRecord = await tx.operationRetries.get(pendingTaskId);
if (retryRecord) {
// Note that we don't reset the lastError, it should still be visible
// while the retry runs.
retryRecord.retryInfo = DbRetryInfo.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: DbRetryInfo.reset(),
};
} else {
if (retryRecord.lastError) {
hadError = true;
}
delete retryRecord.lastError;
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
await tx.operationRetries.put(retryRecord);
if (hadError) {
return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
} else {
return undefined;
}
});
if (maybeNotification) {
ws.notify(maybeNotification);
}
}
async function storePendingTaskFinished(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
await ws.db
.mktx((x) => [x.operationRetries])
.runReadWrite(async (tx) => {
await tx.operationRetries.delete(pendingTaskId);
});
}
export async function runTaskWithErrorReporting(
ws: InternalWalletState,
opId: TaskId,
f: () => Promise<TaskRunResult>,
): Promise<TaskRunResult> {
let maybeError: TalerErrorDetail | undefined;
try {
const resp = await f();
switch (resp.type) {
case TaskRunResultType.Error:
await storePendingTaskError(ws, opId, resp.errorDetail);
return resp;
case TaskRunResultType.Finished:
await storePendingTaskFinished(ws, opId);
return resp;
case TaskRunResultType.Pending:
await storePendingTaskPending(ws, opId);
return resp;
case TaskRunResultType.Longpoll:
return resp;
}
} catch (e) {
if (e instanceof CryptoApiStoppedError) {
if (ws.stopped) {
logger.warn("crypto API stopped during shutdown, ignoring error");
return {
type: TaskRunResultType.Error,
errorDetail: makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
{},
"Crypto API stopped during shutdown",
),
};
}
}
if (e instanceof TalerError) {
logger.warn("operation processed resulted in error");
logger.warn(`error was: ${j2s(e.errorDetail)}`);
maybeError = e.errorDetail;
await storePendingTaskError(ws, opId, maybeError!);
return {
type: TaskRunResultType.Error,
errorDetail: e.errorDetail,
};
} else if (e instanceof Error) {
// This is a bug, as we expect pending operations to always
// do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED
// or return something.
logger.error(`Uncaught exception: ${e.message}`);
logger.error(`Stack: ${e.stack}`);
maybeError = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
{
stack: e.stack,
},
`unexpected exception (message: ${e.message})`,
);
await storePendingTaskError(ws, opId, maybeError);
return {
type: TaskRunResultType.Error,
errorDetail: maybeError,
};
} else {
logger.error("Uncaught exception, value is not even an error.");
maybeError = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
{},
`unexpected exception (not even an error)`,
);
await storePendingTaskError(ws, opId, maybeError);
return {
type: TaskRunResultType.Error,
errorDetail: maybeError,
};
}
}
}
export enum TombstoneTag {
DeleteWithdrawalGroup = "delete-withdrawal-group",
DeleteReserve = "delete-reserve",
DeletePayment = "delete-payment",
DeleteReward = "delete-reward",
DeleteRefreshGroup = "delete-refresh-group",
DeleteDepositGroup = "delete-deposit-group",
DeleteRefund = "delete-refund",
DeletePeerPullDebit = "delete-peer-pull-debit",
DeletePeerPushDebit = "delete-peer-push-debit",
DeletePeerPullCredit = "delete-peer-pull-credit",
DeletePeerPushCredit = "delete-peer-push-credit",
}
export function getExchangeTosStatus(
exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus {
if (!exchangeDetails.tosAccepted) {
return ExchangeTosStatus.Proposed;
}
if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
return ExchangeTosStatus.Proposed;
}
export function makeExchangeListItem(
r: ExchangeEntryRecord,
exchangeDetails: ExchangeDetailsRecord | undefined,
lastError: TalerErrorDetail | undefined,
): ExchangeListItem {
const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
? {
error: lastError,
}
: undefined;
let exchangeUpdateStatus: ExchangeUpdateStatus;
switch (r.updateStatus) {
case ExchangeEntryDbUpdateStatus.Failed:
exchangeUpdateStatus = ExchangeUpdateStatus.Failed;
break;
case ExchangeEntryDbUpdateStatus.Initial:
exchangeUpdateStatus = ExchangeUpdateStatus.Initial;
break;
case ExchangeEntryDbUpdateStatus.InitialUpdate:
exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate;
break;
case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate;
break;
case ExchangeEntryDbUpdateStatus.Ready:
exchangeUpdateStatus = ExchangeUpdateStatus.Ready;
break;
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate;
break;
case ExchangeEntryDbUpdateStatus.Suspended:
exchangeUpdateStatus = ExchangeUpdateStatus.Suspended;
break;
}
let exchangeEntryStatus: ExchangeEntryStatus;
switch (r.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
exchangeEntryStatus = ExchangeEntryStatus.Ephemeral;
break;
case ExchangeEntryDbRecordStatus.Preset:
exchangeEntryStatus = ExchangeEntryStatus.Preset;
break;
case ExchangeEntryDbRecordStatus.Used:
exchangeEntryStatus = ExchangeEntryStatus.Used;
break;
}
return {
exchangeBaseUrl: r.baseUrl,
currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
exchangeUpdateStatus,
exchangeEntryStatus,
tosStatus: exchangeDetails
? getExchangeTosStatus(exchangeDetails)
: ExchangeTosStatus.Pending,
ageRestrictionOptions: exchangeDetails?.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
: [],
paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
lastUpdateErrorInfo,
};
}
export interface LongpollResult {
ready: boolean;
}
export function runLongpollAsync(
ws: InternalWalletState,
retryTag: string,
reqFn: (ct: CancellationToken) => Promise<LongpollResult>,
): void {
const asyncFn = async () => {
if (ws.stopped) {
logger.trace("not long-polling reserve, wallet already stopped");
await storePendingTaskPending(ws, retryTag);
return;
}
const cts = CancellationToken.create();
let res: { ready: boolean } | undefined = undefined;
try {
ws.activeLongpoll[retryTag] = {
cancel: () => {
logger.trace("cancel of reserve longpoll requested");
cts.cancel();
},
};
res = await reqFn(cts.token);
} catch (e) {
const errDetail = getErrorDetailFromException(e);
logger.warn(`got error during long-polling: ${j2s(errDetail)}`);
await storePendingTaskError(ws, retryTag, errDetail);
return;
} finally {
delete ws.activeLongpoll[retryTag];
}
if (!res.ready) {
await storePendingTaskPending(ws, retryTag);
}
ws.workAvailable.trigger();
};
asyncFn();
}
export type ParsedTombstone =
| {
tag: TombstoneTag.DeleteWithdrawalGroup;
withdrawalGroupId: string;
}
| { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
| { tag: TombstoneTag.DeleteReserve; reservePub: string }
| { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
| { tag: TombstoneTag.DeleteReward; walletTipId: string }
| { tag: TombstoneTag.DeletePayment; proposalId: string };
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
switch (p.tag) {
case TombstoneTag.DeleteWithdrawalGroup:
return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteRefund:
return `tmb:${p.tag}:${p.refundGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteReserve:
return `tmb:${p.tag}:${p.reservePub}` as TombstoneIdStr;
case TombstoneTag.DeletePayment:
return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
case TombstoneTag.DeleteRefreshGroup:
return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteReward:
return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
default:
assertUnreachable(p);
}
}
/**
* Uniform interface for a particular wallet transaction.
*/
export interface TransactionManager {
get taskId(): TaskId;
get transactionId(): TransactionIdStr;
fail(): Promise<void>;
abort(): Promise<void>;
suspend(): Promise<void>;
resume(): Promise<void>;
process(): Promise<TaskRunResult>;
}
export enum TaskRunResultType {
Finished = "finished",
Pending = "pending",
Error = "error",
Longpoll = "longpoll",
}
export type TaskRunResult =
| TaskRunFinishedResult
| TaskRunErrorResult
| TaskRunLongpollResult
| TaskRunPendingResult;
export namespace TaskRunResult {
export function finished(): TaskRunResult {
return {
type: TaskRunResultType.Finished,
};
}
export function pending(): TaskRunResult {
return {
type: TaskRunResultType.Pending,
};
}
export function longpoll(): TaskRunResult {
return {
type: TaskRunResultType.Longpoll,
};
}
}
export interface TaskRunFinishedResult {
type: TaskRunResultType.Finished;
}
export interface TaskRunPendingResult {
type: TaskRunResultType.Pending;
}
export interface TaskRunErrorResult {
type: TaskRunResultType.Error;
errorDetail: TalerErrorDetail;
}
export interface TaskRunLongpollResult {
type: TaskRunResultType.Longpoll;
}
export interface DbRetryInfo {
firstTry: DbPreciseTimestamp;
nextRetry: DbPreciseTimestamp;
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: DbRetryInfo,
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 = timestampPreciseToDb(
AbsoluteTime.toPreciseTimestamp(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 = timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
}
export namespace DbRetryInfo {
export function getDuration(
r: DbRetryInfo | 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): DbRetryInfo {
const now = TalerPreciseTimestamp.now();
const info: DbRetryInfo = {
firstTry: timestampPreciseToDb(now),
nextRetry: timestampPreciseToDb(now),
retryCounter: 0,
};
updateTimeout(info, p);
return info;
}
export function increment(
r: DbRetryInfo | undefined,
p: RetryPolicy = defaultRetryPolicy,
): DbRetryInfo {
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; peerPullDebitId: string }
| { tag: PendingTaskType.PeerPullCredit; pursePub: string }
| { tag: PendingTaskType.PeerPushCredit; peerPushCreditId: string }
| { tag: PendingTaskType.PeerPushDebit; pursePub: string }
| { tag: PendingTaskType.Purchase; proposalId: string }
| { tag: PendingTaskType.Recoup; recoupGroupId: string }
| { tag: PendingTaskType.RewardPickup; walletRewardId: 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, peerPullDebitId: rest[0] };
case PendingTaskType.PeerPushCredit:
return { tag: type, peerPushCreditId: 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.RewardPickup:
return { tag: type, walletRewardId: 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.peerPullDebitId}` as TaskId;
case PendingTaskType.PeerPushCredit:
return `${p.tag}:${p.peerPushCreditId}` 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.RewardPickup:
return `${p.tag}:${p.walletRewardId}` 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: ExchangeEntryRecord): TaskId {
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
}
export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
}
export function forTipPickup(tipRecord: RewardRecord): TaskId {
return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` 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: PeerPushDebitRecord,
): TaskId {
return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId;
}
export function forPeerPullPaymentInitiation(
ppi: PeerPullCreditRecord,
): TaskId {
return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId;
}
export function forPeerPullPaymentDebit(
ppi: PeerPullPaymentIncomingRecord,
): TaskId {
return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullDebitId}` as TaskId;
}
export function forPeerPushCredit(
ppi: PeerPushPaymentIncomingRecord,
): TaskId {
return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushCreditId}` as TaskId;
}
}