summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/common.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-02-19 18:05:48 +0100
committerFlorian Dold <florian@dold.me>2024-02-19 18:05:48 +0100
commite951075d2ef52fa8e9e7489c62031777c3a7e66b (patch)
tree64208c09a9162f3a99adccf30edc36de1ef884ef /packages/taler-wallet-core/src/common.ts
parente975740ac4e9ba4bc531226784d640a018c00833 (diff)
downloadwallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.gz
wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.bz2
wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.zip
wallet-core: flatten directory structure
Diffstat (limited to 'packages/taler-wallet-core/src/common.ts')
-rw-r--r--packages/taler-wallet-core/src/common.ts693
1 files changed, 693 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
new file mode 100644
index 000000000..ae1e17ffb
--- /dev/null
+++ b/packages/taler-wallet-core/src/common.ts
@@ -0,0 +1,693 @@
+/*
+ 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,
+ AmountJson,
+ Amounts,
+ CoinRefreshRequest,
+ CoinStatus,
+ Duration,
+ ExchangeEntryState,
+ ExchangeEntryStatus,
+ ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ Logger,
+ RefreshReason,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TombstoneIdStr,
+ TransactionIdStr,
+ durationMul,
+} from "@gnu-taler/taler-util";
+import {
+ BackupProviderRecord,
+ CoinRecord,
+ DbPreciseTimestamp,
+ DepositGroupRecord,
+ ExchangeEntryDbRecordStatus,
+ ExchangeEntryDbUpdateStatus,
+ ExchangeEntryRecord,
+ PeerPullCreditRecord,
+ PeerPullPaymentIncomingRecord,
+ PeerPushDebitRecord,
+ PeerPushPaymentIncomingRecord,
+ PurchaseRecord,
+ RecoupGroupRecord,
+ RefreshGroupRecord,
+ RewardRecord,
+ WalletDbReadWriteTransaction,
+ WithdrawalGroupRecord,
+ timestampPreciseToDb,
+} 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 { createRefreshGroup } from "./refresh.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: WalletDbReadWriteTransaction<["coins", "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: WalletDbReadWriteTransaction<
+ ["coins", "coinAvailability", "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: WalletDbReadWriteTransaction<
+ ["coins", "coinAvailability", "refreshGroups", "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 createRefreshGroup(
+ ws,
+ tx,
+ Amounts.currencyOf(csi.contributions[0]),
+ refreshCoinPubs,
+ csi.refreshReason,
+ csi.allocationId,
+ );
+}
+
+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.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;
+ }
+}
+
+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;
+ }
+}
+
+/**
+ * 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(): TaskId;
+ get transactionId(): TransactionIdStr;
+ fail(): Promise<void>;
+ abort(): Promise<void>;
+ suspend(): Promise<void>;
+ resume(): Promise<void>;
+ process(): Promise<TaskRunResult>;
+}
+
+export enum TaskRunResultType {
+ Finished = "finished",
+ Backoff = "backoff",
+ Progress = "progress",
+ Error = "error",
+ ScheduleLater = "schedule-later",
+}
+
+export type TaskRunResult =
+ | TaskRunFinishedResult
+ | TaskRunErrorResult
+ | TaskRunBackoffResult
+ | TaskRunProgressResult
+ | 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,
+ };
+ }
+}
+
+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 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 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;
+ }
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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): 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.ExchangeUpdate:
+ return `${p.tag}:${encodeURIComponent(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}:${encodeURIComponent(
+ exch.baseUrl,
+ )}` as TaskId;
+ }
+ export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exchBaseUrl,
+ )}` 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}:${encodeURIComponent(
+ 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;
+ }
+}
+
+/**
+ * Result of a transaction transition.
+ */
+export enum TransitionResult {
+ Transition = 1,
+ Stay = 2,
+}
+
+/**
+ * Transaction context.
+ * Uniform interface to all transactions.
+ */
+export interface TransactionContext {
+ abortTransaction(): Promise<void>;
+ suspendTransaction(): Promise<void>;
+ resumeTransaction(): Promise<void>;
+ failTransaction(): Promise<void>;
+ deleteTransaction(): Promise<void>;
+}