aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/common.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/common.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts200
1 files changed, 140 insertions, 60 deletions
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index 6ab6a54d9..abba3f7a7 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -26,6 +26,7 @@ import {
CoinRefreshRequest,
CoinStatus,
Duration,
+ ExchangeEntryState,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
@@ -75,7 +76,10 @@ 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";
+import {
+ constructTransactionIdentifier,
+ parseTransactionIdentifier,
+} from "./transactions.js";
const logger = new Logger("operations/common.ts");
@@ -320,11 +324,7 @@ function convertTaskToTransactionId(
}
}
-/**
- * For tasks that process a transaction,
- * generate a state transition notification.
- */
-async function taskToTransactionNotification(
+async function makeTransactionRetryNotification(
ws: InternalWalletState,
tx: GetReadOnlyAccess<typeof WalletStoresV1>,
pendingTaskId: string,
@@ -353,6 +353,75 @@ async function taskToTransactionNotification(
return notif;
}
+async function makeExchangeRetryNotification(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ logger.info("making exchange retry notification");
+ const parsedTaskId = parseTaskIdentifier(pendingTaskId);
+ if (parsedTaskId.tag !== PendingTaskType.ExchangeUpdate) {
+ throw Error("invalid task identifier");
+ }
+ const rec = await tx.exchanges.get(parsedTaskId.exchangeBaseUrl);
+
+ if (!rec) {
+ logger.info(`exchange ${parsedTaskId.exchangeBaseUrl} not found`);
+ return undefined;
+ }
+
+ const notif: WalletNotification = {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: parsedTaskId.exchangeBaseUrl,
+ oldExchangeState: getExchangeState(rec),
+ newExchangeState: getExchangeState(rec),
+ };
+ if (e) {
+ notif.errorInfo = {
+ code: e.code as number,
+ hint: e.hint,
+ };
+ }
+ return notif;
+}
+
+/**
+ * Generate an appropriate error transition notification
+ * for applicable tasks.
+ *
+ * Namely, transition notifications are generated for:
+ * - exchange update errors
+ * - transactions
+ */
+async function taskToRetryNotification(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ const parsedTaskId = parseTaskIdentifier(pendingTaskId);
+
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.ExchangeUpdate:
+ return makeExchangeRetryNotification(ws, tx, pendingTaskId, e);
+ case PendingTaskType.PeerPullCredit:
+ case PendingTaskType.PeerPullDebit:
+ case PendingTaskType.Withdraw:
+ case PendingTaskType.PeerPushCredit:
+ case PendingTaskType.Deposit:
+ case PendingTaskType.Refresh:
+ case PendingTaskType.RewardPickup:
+ case PendingTaskType.PeerPushDebit:
+ case PendingTaskType.Purchase:
+ return makeTransactionRetryNotification(ws, tx, pendingTaskId, e);
+ case PendingTaskType.Backup:
+ case PendingTaskType.ExchangeCheckRefresh:
+ case PendingTaskType.Recoup:
+ return undefined;
+ }
+}
+
async function storePendingTaskError(
ws: InternalWalletState,
pendingTaskId: string,
@@ -372,7 +441,7 @@ async function storePendingTaskError(
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
await tx.operationRetries.put(retryRecord);
- return taskToTransactionNotification(ws, tx, pendingTaskId, e);
+ return taskToRetryNotification(ws, tx, pendingTaskId, e);
});
if (maybeNotification) {
ws.notify(maybeNotification);
@@ -391,7 +460,7 @@ export async function resetPendingTaskTimeout(
retryRecord.retryInfo = DbRetryInfo.reset();
await tx.operationRetries.put(retryRecord);
}
- return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+ return taskToRetryNotification(ws, tx, pendingTaskId, undefined);
});
if (maybeNotification) {
ws.notify(maybeNotification);
@@ -419,7 +488,7 @@ async function storePendingTaskPending(
}
await tx.operationRetries.put(retryRecord);
if (hadError) {
- return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+ return taskToRetryNotification(ws, tx, pendingTaskId, undefined);
} else {
return undefined;
}
@@ -532,66 +601,72 @@ export enum TombstoneTag {
DeletePeerPushCredit = "delete-peer-push-credit",
}
-export function getExchangeTosStatus(
- exchangeDetails: ExchangeDetailsRecord,
+export function getExchangeTosStatusFromRecord(
+ exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
- if (!exchangeDetails.tosAccepted) {
+ if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
- if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
+ if (exchange.tosAcceptedEtag == exchange.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
return ExchangeTosStatus.Proposed;
}
-export function makeExchangeListItem(
+export function getExchangeUpdateStatusFromRecord(
r: ExchangeEntryRecord,
- exchangeDetails: ExchangeDetailsRecord | undefined,
- lastError: TalerErrorDetail | undefined,
-): ExchangeListItem {
- const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
- ? {
- error: lastError,
- }
- : undefined;
-
- let exchangeUpdateStatus: ExchangeUpdateStatus;
+): ExchangeUpdateStatus {
switch (r.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Failed:
- exchangeUpdateStatus = ExchangeUpdateStatus.Failed;
- break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ return ExchangeUpdateStatus.UnavailableUpdate;
case ExchangeEntryDbUpdateStatus.Initial:
- exchangeUpdateStatus = ExchangeUpdateStatus.Initial;
- break;
+ return ExchangeUpdateStatus.Initial;
case ExchangeEntryDbUpdateStatus.InitialUpdate:
- exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate;
- break;
- case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
- exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate;
- break;
+ return ExchangeUpdateStatus.InitialUpdate;
case ExchangeEntryDbUpdateStatus.Ready:
- exchangeUpdateStatus = ExchangeUpdateStatus.Ready;
- break;
+ return ExchangeUpdateStatus.Ready;
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate;
- break;
+ return ExchangeUpdateStatus.ReadyUpdate;
case ExchangeEntryDbUpdateStatus.Suspended:
- exchangeUpdateStatus = ExchangeUpdateStatus.Suspended;
- break;
+ return ExchangeUpdateStatus.Suspended;
}
+}
- let exchangeEntryStatus: ExchangeEntryStatus;
+export function getExchangeEntryStatusFromRecord(
+ r: ExchangeEntryRecord,
+): ExchangeEntryStatus {
switch (r.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
- exchangeEntryStatus = ExchangeEntryStatus.Ephemeral;
- break;
+ return ExchangeEntryStatus.Ephemeral;
case ExchangeEntryDbRecordStatus.Preset:
- exchangeEntryStatus = ExchangeEntryStatus.Preset;
- break;
+ return ExchangeEntryStatus.Preset;
case ExchangeEntryDbRecordStatus.Used:
- exchangeEntryStatus = ExchangeEntryStatus.Used;
- break;
+ 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 function makeExchangeListItem(
+ r: ExchangeEntryRecord,
+ exchangeDetails: ExchangeDetailsRecord | undefined,
+ lastError: TalerErrorDetail | undefined,
+): ExchangeListItem {
+ const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
+ ? {
+ error: lastError,
+ }
+ : undefined;
let scopeInfo: ScopeInfo | undefined = undefined;
if (exchangeDetails) {
@@ -606,11 +681,9 @@ export function makeExchangeListItem(
return {
exchangeBaseUrl: r.baseUrl,
currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
- exchangeUpdateStatus,
- exchangeEntryStatus,
- tosStatus: exchangeDetails
- ? getExchangeTosStatus(exchangeDetails)
- : ExchangeTosStatus.Pending,
+ exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
+ exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ tosStatus: getExchangeTosStatusFromRecord(r),
ageRestrictionOptions: exchangeDetails?.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
: [],
@@ -852,7 +925,6 @@ export type ParsedTaskIdentifier =
| { 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 }
@@ -872,13 +944,13 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
const [type, ...rest] = task;
switch (type) {
case PendingTaskType.Backup:
- return { tag: type, backupProviderBaseUrl: rest[0] };
+ return { tag: type, backupProviderBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.Deposit:
return { tag: type, depositGroupId: rest[0] };
case PendingTaskType.ExchangeCheckRefresh:
- return { tag: type, exchangeBaseUrl: rest[0] };
+ return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.ExchangeUpdate:
- return { tag: type, exchangeBaseUrl: rest[0] };
+ return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.PeerPullCredit:
return { tag: type, pursePub: rest[0] };
case PendingTaskType.PeerPullDebit:
@@ -940,13 +1012,19 @@ export namespace TaskIdentifiers {
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
}
export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId {
- return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exch.baseUrl,
+ )}` as TaskId;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
- return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exchBaseUrl,
+ )}` as TaskId;
}
export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {
- return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
+ return `${PendingTaskType.ExchangeCheckRefresh}:${encodeURIComponent(
+ exch.baseUrl,
+ )}` as TaskId;
}
export function forTipPickup(tipRecord: RewardRecord): TaskId {
return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskId;
@@ -964,7 +1042,9 @@ export namespace TaskIdentifiers {
return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskId;
}
export function forBackup(backupRecord: BackupProviderRecord): TaskId {
- return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
+ return `${PendingTaskType.Backup}:${encodeURIComponent(
+ backupRecord.baseUrl,
+ )}` as TaskId;
}
export function forPeerPushPaymentInitiation(
ppi: PeerPushDebitRecord,