/*
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
*/
/**
* Imports.
*/
import {
AbsoluteTime,
AmountJson,
Amounts,
AsyncFlag,
CoinRefreshRequest,
CoinStatus,
Duration,
ExchangeEntryState,
ExchangeEntryStatus,
ExchangeTosStatus,
ExchangeUpdateStatus,
Logger,
RefreshReason,
TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TombstoneIdStr,
TransactionIdStr,
WalletNotification,
assertUnreachable,
checkDbInvariant,
checkLogicInvariant,
durationMul,
} from "@gnu-taler/taler-util";
import {
BackupProviderRecord,
CoinHistoryRecord,
CoinRecord,
DbPreciseTimestamp,
DepositGroupRecord,
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
PeerPullCreditRecord,
PeerPullPaymentIncomingRecord,
PeerPushDebitRecord,
PeerPushPaymentIncomingRecord,
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
WalletDbReadWriteTransaction,
WithdrawalGroupRecord,
timestampPreciseToDb,
} from "./db.js";
import { ReadyExchangeSummary } from "./exchanges.js";
import { createRefreshGroup } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
const logger = new Logger("operations/common.ts");
export interface CoinsSpendInfo {
coinPubs: string[];
contributions: AmountJson[];
refreshReason: RefreshReason;
/**
* Transaction for which the coin is spent.
*/
transactionId: TransactionIdStr;
}
export async function makeCoinsVisible(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<["coins", "coinAvailability"]>,
transactionId: string,
): Promise {
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(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
["coins", "coinAvailability", "denominations"]
>,
coinRecord: CoinRecord,
): Promise {
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,
`denomination of a coin is missing hash: ${coinRecord.denomPubHash}`,
);
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);
}
/**
* Spend coins. Marks the coins as used, adds a coin history items
* and creates refresh group.
*/
export async function spendCoins(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
[
"coins",
"coinHistory",
"coinAvailability",
"refreshGroups",
"refreshSessions",
"denominations",
]
>,
csi: CoinsSpendInfo,
): Promise {
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 getDenomInfo(
wex,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
);
checkDbInvariant(
!!denom,
`denomination of a coin is missing hash: ${coin.denomPubHash}`,
);
const coinAvailability = await tx.coinAvailability.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
coin.maxAge,
]);
checkDbInvariant(
!!coinAvailability,
`age denom info is missing for ${coin.maxAge}`,
);
const contrib = csi.contributions[i];
coin.status = CoinStatus.Dormant;
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,
});
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--;
}
}
let histEntry: CoinHistoryRecord | undefined = await tx.coinHistory.get(
coin.coinPub,
);
if (!histEntry) {
histEntry = {
coinPub: coin.coinPub,
history: [],
};
}
histEntry.history.push({
type: "spend",
transactionId: csi.transactionId,
amount: Amounts.stringify(contrib),
});
await tx.coinHistory.put(histEntry);
await tx.coins.put(coin);
await tx.coinAvailability.put(coinAvailability);
}
await createRefreshGroup(
wex,
tx,
Amounts.currencyOf(csi.contributions[0]),
refreshCoinPubs,
csi.refreshReason,
csi.transactionId,
);
}
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 getExchangeTosStatusFromRecord(
exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
if (exchange.tosCurrentEtag == null) {
return ExchangeTosStatus.MissingTos;
}
if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
if (exchange.tosAcceptedEtag == exchange.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
return ExchangeTosStatus.Proposed;
}
export function getExchangeUpdateStatusFromRecord(
r: ExchangeEntryRecord,
): ExchangeUpdateStatus {
switch (r.updateStatus) {
case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
return ExchangeUpdateStatus.UnavailableUpdate;
case ExchangeEntryDbUpdateStatus.Initial:
return ExchangeUpdateStatus.Initial;
case ExchangeEntryDbUpdateStatus.InitialUpdate:
return ExchangeUpdateStatus.InitialUpdate;
case ExchangeEntryDbUpdateStatus.Ready:
return ExchangeUpdateStatus.Ready;
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
return ExchangeUpdateStatus.ReadyUpdate;
case ExchangeEntryDbUpdateStatus.Suspended:
return ExchangeUpdateStatus.Suspended;
default:
assertUnreachable(r.updateStatus);
}
}
export function getExchangeEntryStatusFromRecord(
r: ExchangeEntryRecord,
): ExchangeEntryStatus {
switch (r.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
return ExchangeEntryStatus.Ephemeral;
case ExchangeEntryDbRecordStatus.Preset:
return ExchangeEntryStatus.Preset;
case ExchangeEntryDbRecordStatus.Used:
return ExchangeEntryStatus.Used;
default:
assertUnreachable(r.entryStatus);
}
}
/**
* Compute the state of an exchange entry from the DB
* record.
*/
export function getExchangeState(r: ExchangeEntryRecord): ExchangeEntryState {
return {
exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
tosStatus: getExchangeTosStatusFromRecord(r),
};
}
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(): TaskIdStr;
get transactionId(): TransactionIdStr;
fail(): Promise;
abort(): Promise;
suspend(): Promise;
resume(): Promise;
process(): Promise;
}
export enum TaskRunResultType {
Finished = "finished",
Backoff = "backoff",
Progress = "progress",
Error = "error",
LongpollReturnedPending = "longpoll-returned-pending",
ScheduleLater = "schedule-later",
}
export type TaskRunResult =
| TaskRunFinishedResult
| TaskRunErrorResult
| TaskRunBackoffResult
| TaskRunProgressResult
| TaskRunLongpollReturnedPendingResult
| TaskRunScheduleLaterResult;
export namespace TaskRunResult {
/**
* Task is finished and does not need to be processed again.
*/
export function finished(): TaskRunResult {
return {
type: TaskRunResultType.Finished,
};
}
/**
* Task is waiting for something, should be invoked
* again with exponentiall back-off until some other
* result is returned.
*/
export function backoff(): TaskRunResult {
return {
type: TaskRunResultType.Backoff,
};
}
/**
* Task made progress and should be processed again.
*/
export function progress(): TaskRunResult {
return {
type: TaskRunResultType.Progress,
};
}
/**
* Run the task again at a fixed time in the future.
*/
export function runAgainAt(runAt: AbsoluteTime): TaskRunResult {
return {
type: TaskRunResultType.ScheduleLater,
runAt,
};
}
/**
* Longpolling returned, but what we're waiting for
* is still pending on the other side.
*/
export function longpollReturnedPending(): TaskRunLongpollReturnedPendingResult {
return {
type: TaskRunResultType.LongpollReturnedPending,
};
}
}
export interface TaskRunFinishedResult {
type: TaskRunResultType.Finished;
}
export interface TaskRunBackoffResult {
type: TaskRunResultType.Backoff;
}
export interface TaskRunProgressResult {
type: TaskRunResultType.Progress;
}
export interface TaskRunScheduleLaterResult {
type: TaskRunResultType.ScheduleLater;
runAt: AbsoluteTime;
}
export interface TaskRunLongpollReturnedPendingResult {
type: TaskRunResultType.LongpollReturnedPending;
}
export interface TaskRunErrorResult {
type: TaskRunResultType.Error;
errorDetail: TalerErrorDetail;
}
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 function computeDbBackoff(retryCounter: number): DbPreciseTimestamp {
const now = AbsoluteTime.now();
if (now.t_ms === "never") {
throw Error("assertion failed");
}
const p = defaultRetryPolicy;
if (p.backoffDelta.d_ms === "forever") {
throw Error("assertion failed");
}
const nextIncrement =
p.backoffDelta.d_ms * Math.pow(p.backoffBase, retryCounter);
const t =
now.t_ms +
(p.maxTimeout.d_ms === "forever"
? nextIncrement
: Math.min(p.maxTimeout.d_ms, nextIncrement));
return timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
}
export namespace DbRetryInfo {
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;
}
}
/**
* Timestamp after which the wallet would do an auto-refresh.
*/
export function getAutoRefreshExecuteThreshold(d: {
stampExpireWithdraw: TalerProtocolTimestamp;
stampExpireDeposit: TalerProtocolTimestamp;
}): AbsoluteTime {
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
d.stampExpireWithdraw,
);
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
d.stampExpireDeposit,
);
const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
const deltaDiv = durationMul(delta, 0.5);
return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
}
/**
* Type and schema definitions for pending tasks in the wallet.
*
* These are only used internally, and are not part of the stable public
* interface to the wallet.
*/
export enum PendingTaskType {
ExchangeUpdate = "exchange-update",
Purchase = "purchase",
Refresh = "refresh",
Recoup = "recoup",
RewardPickup = "reward-pickup",
Withdraw = "withdraw",
Deposit = "deposit",
Backup = "backup",
PeerPushDebit = "peer-push-debit",
PeerPullCredit = "peer-pull-credit",
PeerPushCredit = "peer-push-credit",
PeerPullDebit = "peer-pull-debit",
}
/**
* 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.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: decodeURIComponent(rest[0]) };
case PendingTaskType.Deposit:
return { tag: type, depositGroupId: rest[0] };
case PendingTaskType.ExchangeUpdate:
return { tag: type, exchangeBaseUrl: decodeURIComponent(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): TaskIdStr {
switch (p.tag) {
case PendingTaskType.Backup:
return `${p.tag}:${p.backupProviderBaseUrl}` as TaskIdStr;
case PendingTaskType.Deposit:
return `${p.tag}:${p.depositGroupId}` as TaskIdStr;
case PendingTaskType.ExchangeUpdate:
return `${p.tag}:${encodeURIComponent(p.exchangeBaseUrl)}` as TaskIdStr;
case PendingTaskType.PeerPullDebit:
return `${p.tag}:${p.peerPullDebitId}` as TaskIdStr;
case PendingTaskType.PeerPushCredit:
return `${p.tag}:${p.peerPushCreditId}` as TaskIdStr;
case PendingTaskType.PeerPullCredit:
return `${p.tag}:${p.pursePub}` as TaskIdStr;
case PendingTaskType.PeerPushDebit:
return `${p.tag}:${p.pursePub}` as TaskIdStr;
case PendingTaskType.Purchase:
return `${p.tag}:${p.proposalId}` as TaskIdStr;
case PendingTaskType.Recoup:
return `${p.tag}:${p.recoupGroupId}` as TaskIdStr;
case PendingTaskType.Refresh:
return `${p.tag}:${p.refreshGroupId}` as TaskIdStr;
case PendingTaskType.RewardPickup:
return `${p.tag}:${p.walletRewardId}` as TaskIdStr;
case PendingTaskType.Withdraw:
return `${p.tag}:${p.withdrawalGroupId}` as TaskIdStr;
default:
assertUnreachable(p);
}
}
export namespace TaskIdentifiers {
export function forWithdrawal(wg: WithdrawalGroupRecord): TaskIdStr {
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskIdStr;
}
export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskIdStr {
return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
exch.baseUrl,
)}` as TaskIdStr;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskIdStr {
return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
exchBaseUrl,
)}` as TaskIdStr;
}
export function forRefresh(
refreshGroupRecord: RefreshGroupRecord,
): TaskIdStr {
return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskIdStr;
}
export function forPay(purchaseRecord: PurchaseRecord): TaskIdStr {
return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as TaskIdStr;
}
export function forRecoup(recoupRecord: RecoupGroupRecord): TaskIdStr {
return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskIdStr;
}
export function forDeposit(depositRecord: DepositGroupRecord): TaskIdStr {
return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskIdStr;
}
export function forBackup(backupRecord: BackupProviderRecord): TaskIdStr {
return `${PendingTaskType.Backup}:${encodeURIComponent(
backupRecord.baseUrl,
)}` as TaskIdStr;
}
export function forPeerPushPaymentInitiation(
ppi: PeerPushDebitRecord,
): TaskIdStr {
return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskIdStr;
}
export function forPeerPullPaymentInitiation(
ppi: PeerPullCreditRecord,
): TaskIdStr {
return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskIdStr;
}
export function forPeerPullPaymentDebit(
ppi: PeerPullPaymentIncomingRecord,
): TaskIdStr {
return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullDebitId}` as TaskIdStr;
}
export function forPeerPushCredit(
ppi: PeerPushPaymentIncomingRecord,
): TaskIdStr {
return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushCreditId}` as TaskIdStr;
}
}
/**
* Result of a transaction transition.
*/
export enum TransitionResultType {
Transition = 1,
Stay = 2,
Delete = 3,
}
export type TransitionResult =
| { type: TransitionResultType.Stay }
| { type: TransitionResultType.Transition; rec: R }
| { type: TransitionResultType.Delete };
export const TransitionResult = {
stay(): TransitionResult {
return { type: TransitionResultType.Stay };
},
delete(): TransitionResult {
return { type: TransitionResultType.Delete };
},
transition(rec: T): TransitionResult {
return {
type: TransitionResultType.Transition,
rec,
};
},
};
/**
* Transaction context.
* Uniform interface to all transactions.
*/
export interface TransactionContext {
get taskId(): TaskIdStr | undefined;
get transactionId(): TransactionIdStr;
abortTransaction(): Promise;
suspendTransaction(): Promise;
resumeTransaction(): Promise;
failTransaction(): Promise;
deleteTransaction(): Promise;
}
declare const __taskIdStr: unique symbol;
export type TaskIdStr = string & { [__taskIdStr]: true };
/**
* Wait until the wallet is in a particular state.
*
* Two functions must be provided:
* 1. checkState, which checks if the wallet is in the
* desired state.
* 2. filterNotification, which checks whether a notification
* might have lead to a state change.
*/
export async function genericWaitForState(
wex: WalletExecutionContext,
args: {
checkState: () => Promise;
filterNotification: (notif: WalletNotification) => boolean;
},
): Promise {
await wex.taskScheduler.ensureRunning();
// FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
const flag = new AsyncFlag();
// Raise purchaseNotifFlag whenever we get a notification
// about our refresh.
const cancelNotif = wex.ws.addNotificationListener((notif) => {
if (args.filterNotification(notif)) {
flag.raise();
}
});
const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => {
cancelNotif();
flag.raise();
});
try {
while (true) {
if (wex.cancellationToken.isCancelled) {
throw Error("cancelled");
}
if (await args.checkState()) {
return;
}
// Wait for the next transition
await flag.wait();
flag.reset();
}
} catch (e) {
unregisterOnCancelled();
cancelNotif();
throw e;
}
}
export function requireExchangeTosAcceptedOrThrow(
exchange: ReadyExchangeSummary,
): void {
switch (exchange.tosStatus) {
case ExchangeTosStatus.Accepted:
case ExchangeTosStatus.MissingTos:
break;
default:
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED,
{
exchangeBaseUrl: exchange.exchangeBaseUrl,
currentEtag: exchange.tosCurrentEtag,
tosStatus: exchange.tosStatus,
},
);
}
}