diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/common.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/common.ts | 200 |
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, |