aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-12-11 20:01:28 +0100
committerFlorian Dold <florian@dold.me>2023-12-12 15:42:34 +0100
commite31f18b8f129adb9cbe33158297a9cff56a7143e (patch)
treefc960e069a08ca1924a79c154f5ced26db709348
parent055645e17aa9424f299aa04f686de7574ab437c7 (diff)
wallet-core: towards better DD48 support
-rw-r--r--API_CHANGES.md4
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-unoffered.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance.ts4
-rw-r--r--packages/taler-util/src/notifications.ts33
-rw-r--r--packages/taler-util/src/wallet-types.ts17
-rw-r--r--packages/taler-wallet-cli/src/index.ts3
-rw-r--r--packages/taler-wallet-core/src/db.ts28
-rw-r--r--packages/taler-wallet-core/src/dbless.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts200
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts723
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts29
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts12
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts10
-rw-r--r--packages/taler-wallet-core/src/wallet.ts262
19 files changed, 838 insertions, 520 deletions
diff --git a/API_CHANGES.md b/API_CHANGES.md
index 6fceca31c..f53daf598 100644
--- a/API_CHANGES.md
+++ b/API_CHANGES.md
@@ -23,3 +23,7 @@ This files contains all the API changes for the current release:
via a taler://withdraw-exchange URI.
- 2023-12-11 dold: Add exchangeBaseUrl to the checkPeerPushDebit response.
- 2023-12-11 dold: Add scopeInfo to exchange entry list items.
+- BREAK 2023-12-12 dold: Remove forceUpdate and masterPub arguments from addExchange
+ request. This request has previously been overloaded both to update an
+ exchange entry as well as to add it.
+ To update the entry, updateExchangeEntry should be used instead. \ No newline at end of file
diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
index 259cc33f9..1a62a6065 100644
--- a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
+++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
@@ -127,9 +127,9 @@ export async function runDenomUnofferedTest(t: GlobalTestState) {
// TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,
// );
- await walletClient.call(WalletApiOperation.AddExchange, {
+ // Force updating the exchange entry so that the wallet knows about the new denominations.
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: exchange.baseUrl,
- forceUpdate: true,
});
await walletClient.call(WalletApiOperation.DeleteTransaction, {
diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
index 9ed2d6206..6b47951bc 100644
--- a/packages/taler-harness/src/integrationtests/test-revocation.ts
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -180,9 +180,8 @@ export async function runRevocationTest(t: GlobalTestState) {
// FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565
// is implemented.
- await walletClient.call(WalletApiOperation.AddExchange, {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: exchange.baseUrl,
- forceUpdate: true,
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
@@ -218,9 +217,8 @@ export async function runRevocationTest(t: GlobalTestState) {
// FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565
// is implemented.
- await walletClient.call(WalletApiOperation.AddExchange, {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: exchange.baseUrl,
- forceUpdate: true,
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
{
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
index 0e57ce477..c4ca94dc0 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
@@ -75,6 +75,8 @@ export async function runWalletBalanceTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
+ console.log("creating order");
+
const orderResp = await merchantClient.createOrder({
order,
});
@@ -117,6 +119,8 @@ export async function runWalletBalanceTest(t: GlobalTestState) {
Amounts.isZero(preparePayResult.balanceDetails.balanceMerchantDepositable),
);
+ console.log("waiting for transactions to finalize");
+
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index b91d91777..571d8f036 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -23,15 +23,14 @@
* Imports.
*/
import { TransactionState } from "./transactions-types.js";
-import { TalerErrorDetail } from "./wallet-types.js";
+import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
export enum NotificationType {
BalanceChange = "balance-change",
- ExchangeOperationError = "exchange-operation-error",
- ExchangeAdded = "exchange-added",
BackupOperationError = "backup-error",
PendingOperationProcessed = "pending-operation-processed",
TransactionStateTransition = "transaction-state-transition",
+ ExchangeStateTransition = "exchange-state-transition",
}
export interface ErrorInfoSummary {
@@ -59,19 +58,29 @@ export interface TransactionStateTransitionNotification {
experimentalUserData?: any;
}
-export interface ExchangeAddedNotification {
- type: NotificationType.ExchangeAdded;
+export interface ExchangeStateTransitionNotification {
+ type: NotificationType.ExchangeStateTransition;
+ /**
+ * Identification of the exchange entry that this
+ * notification is about.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * If missing, the notification means that
+ * the exchange entry is newly created.
+ */
+ oldExchangeState?: ExchangeEntryState;
+
+ newExchangeState: ExchangeEntryState;
+
+ errorInfo?: ErrorInfoSummary;
}
export interface BalanceChangeNotification {
type: NotificationType.BalanceChange;
}
-export interface ExchangeOperationErrorNotification {
- type: NotificationType.ExchangeOperationError;
- error: TalerErrorDetail;
-}
-
export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError;
error: TalerErrorDetail;
@@ -80,12 +89,12 @@ export interface BackupOperationErrorNotification {
export interface PendingOperationProcessedNotification {
type: NotificationType.PendingOperationProcessed;
id: string;
+ taskResultType: string;
}
export type WalletNotification =
| BalanceChangeNotification
| BackupOperationErrorNotification
- | ExchangeAddedNotification
- | ExchangeOperationErrorNotification
+ | ExchangeStateTransitionNotification
| PendingOperationProcessedNotification
| TransactionStateTransitionNotification;
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 82c58246a..aa498c409 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -1289,12 +1289,11 @@ export enum ExchangeEntryStatus {
export enum ExchangeUpdateStatus {
Initial = "initial",
- InitialUpdate = "initial(update)",
+ InitialUpdate = "initial-update",
Suspended = "suspended",
- Failed = "failed",
- OutdatedUpdate = "outdated(update)",
+ UnavailableUpdate = "unavailable-update",
Ready = "ready",
- ReadyUpdate = "ready(update)",
+ ReadyUpdate = "ready-update",
}
export interface OperationErrorInfo {
@@ -1645,15 +1644,11 @@ export type GetExchangeEntryByUrlResponse = ExchangeListItem;
export interface AddExchangeRequest {
exchangeBaseUrl: string;
- masterPub?: string;
- forceUpdate?: boolean;
}
export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
.property("exchangeBaseUrl", codecForString())
- .property("forceUpdate", codecOptional(codecForBoolean()))
- .property("masterPub", codecOptional(codecForString()))
.build("AddExchangeRequest");
export interface UpdateExchangeEntryRequest {
@@ -2875,3 +2870,9 @@ export interface PrepareWithdrawExchangeResponse {
*/
amount?: AmountString;
}
+
+export interface ExchangeEntryState {
+ tosStatus: ExchangeTosStatus;
+ exchangeEntryStatus: ExchangeEntryStatus;
+ exchangeUpdateStatus: ExchangeUpdateStatus;
+}
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index ea250de19..8a8f9737a 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -800,9 +800,8 @@ exchangesCli
.flag("force", ["-f", "--force"])
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.AddExchange, {
+ await wallet.client.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: args.exchangesUpdateCmd.url,
- forceUpdate: args.exchangesUpdateCmd.force,
});
});
});
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 6b1fc2f5f..d2c6b8368 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -569,21 +569,6 @@ export interface ExchangeDetailsRecord {
*/
globalFees: ExchangeGlobalFees[];
- /**
- * Etag of the current ToS of the exchange.
- */
- tosCurrentEtag: string;
-
- /**
- * Information about ToS acceptance from the user.
- */
- tosAccepted:
- | {
- etag: string;
- timestamp: DbPreciseTimestamp;
- }
- | undefined;
-
wireInfo: WireInfo;
/**
@@ -615,8 +600,8 @@ export enum ExchangeEntryDbUpdateStatus {
Initial = 1,
InitialUpdate = 2,
Suspended = 3,
- Failed = 4,
- OutdatedUpdate = 5,
+ UnavailableUpdate = 4,
+ // Reserved 5 for backwards compatibility.
Ready = 6,
ReadyUpdate = 7,
}
@@ -660,6 +645,15 @@ export interface ExchangeEntryRecord {
updateStatus: ExchangeEntryDbUpdateStatus;
/**
+ * Etag of the current ToS of the exchange.
+ */
+ tosCurrentEtag: string | undefined;
+
+ tosAcceptedEtag: string | undefined;
+
+ tosAcceptedTimestamp: DbPreciseTimestamp | undefined;
+
+ /**
* Last time when the exchange /keys info was updated.
*/
lastUpdate: DbPreciseTimestamp | undefined;
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index 7f4e9ca9b..e841d1d20 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -59,8 +59,11 @@ import {
} from "@gnu-taler/taler-util/http";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { DenominationRecord } from "./db.js";
-import { isWithdrawableDenom } from "./index.js";
-import { ExchangeInfo } from "./operations/exchanges.js";
+import {
+ ExchangeInfo,
+ ExchangeKeysDownloadResult,
+ isWithdrawableDenom,
+} from "./index.js";
import { assembleRefreshRevealRequest } from "./operations/refresh.js";
import {
getBankStatusUrl,
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,
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 622f04bd3..253801e93 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -15,6 +15,12 @@
*/
/**
+ * @fileoverview
+ * Implementation of exchange entry management in wallet-core.
+ * The details of exchange entry management are specified in DD48.
+ */
+
+/**
* Imports.
*/
import {
@@ -31,7 +37,9 @@ import {
ExchangeAuditor,
ExchangeGlobalFees,
ExchangeSignKeyJson,
+ ExchangeEntryState,
ExchangeWireAccount,
+ GetExchangeTosResult,
GlobalFees,
hashDenomPub,
j2s,
@@ -48,10 +56,17 @@ import {
TalerProtocolDuration,
TalerProtocolTimestamp,
URL,
+ WalletNotification,
WireFee,
WireFeeMap,
WireFeesJson,
WireInfo,
+ FeeDescription,
+ DenomOperationMap,
+ DenominationInfo,
+ ExchangeDetailedResponse,
+ ExchangeListItem,
+ ExchangesListResponse,
} from "@gnu-taler/taler-util";
import {
getExpiry,
@@ -67,24 +82,29 @@ import {
WalletStoresV1,
} from "../db.js";
import {
+ createTimeline,
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
isWithdrawableDenom,
+ OpenedPromise,
+ openPromise,
+ selectBestForOverlappingDenominations,
+ selectMinimumFee,
timestampPreciseFromDb,
timestampPreciseToDb,
timestampProtocolToDb,
WalletDbReadWriteTransaction,
} from "../index.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
+import { CancelFn, InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
-import {
- DbAccess,
- GetReadOnlyAccess,
- GetReadWriteAccess,
-} from "../util/query.js";
+import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
import {
- runTaskWithErrorReporting,
+ getExchangeEntryStatusFromRecord,
+ getExchangeState,
+ getExchangeTosStatusFromRecord,
+ getExchangeUpdateStatusFromRecord,
+ makeExchangeListItem,
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
@@ -92,19 +112,19 @@ import {
const logger = new Logger("exchanges.ts");
-export function getExchangeRequestTimeout(): Duration {
+function getExchangeRequestTimeout(): Duration {
return Duration.fromSpec({
seconds: 5,
});
}
-export interface ExchangeTosDownloadResult {
+interface ExchangeTosDownloadResult {
tosText: string;
tosEtag: string;
tosContentType: string;
}
-export async function downloadExchangeWithTermsOfService(
+async function downloadExchangeWithTermsOfService(
exchangeBaseUrl: string,
http: HttpRequestLibrary,
timeout: Duration,
@@ -129,6 +149,8 @@ export async function downloadExchangeWithTermsOfService(
/**
* Get exchange details from the database.
+ *
+ * FIXME: Should we encapsulate the result better, instead of returning the raw DB records here?
*/
export async function getExchangeDetails(
tx: GetReadOnlyAccess<{
@@ -153,9 +175,6 @@ export async function getExchangeDetails(
]);
}
-getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
- db.mktx((x) => [x.exchanges, x.exchangeDetails]);
-
/**
* Mark a ToS version as accepted by the user.
*
@@ -169,13 +188,13 @@ export async function acceptExchangeTermsOfService(
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => {
- const d = await getExchangeDetails(tx, exchangeBaseUrl);
- if (d) {
- d.tosAccepted = {
- etag: etag || d.tosCurrentEtag,
- timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
- };
- await tx.exchangeDetails.put(d);
+ const exch = await tx.exchanges.get(exchangeBaseUrl);
+ if (exch && exch.tosCurrentEtag) {
+ exch.tosAcceptedEtag = exch.tosCurrentEtag;
+ exch.tosAcceptedTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ await tx.exchanges.put(exch);
}
});
}
@@ -284,29 +303,18 @@ async function validateGlobalFees(
return egf;
}
-export interface ExchangeInfo {
- keys: ExchangeKeysDownloadResult;
-}
-
-export async function downloadExchangeInfo(
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
-): Promise<ExchangeInfo> {
- const keysInfo = await downloadExchangeKeysInfo(
- exchangeBaseUrl,
- http,
- Duration.getForever(),
- );
- return {
- keys: keysInfo,
- };
-}
-
+/**
+ * Add an exchange entry to the wallet database in the
+ * entry state "preset".
+ *
+ * Returns the notification to the caller that should be emitted
+ * if the DB transaction succeeds.
+ */
export async function addPresetExchangeEntry(
tx: WalletDbReadWriteTransaction<"exchanges">,
exchangeBaseUrl: string,
currencyHint?: string,
-): Promise<void> {
+): Promise<{ notification?: WalletNotification }> {
let exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange) {
const r: ExchangeEntryRecord = {
@@ -323,9 +331,22 @@ export async function addPresetExchangeEntry(
nextUpdateStamp: timestampPreciseToDb(
AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
),
+ tosAcceptedEtag: undefined,
+ tosAcceptedTimestamp: undefined,
+ tosCurrentEtag: undefined,
};
await tx.exchanges.put(r);
+ return {
+ notification: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: exchangeBaseUrl,
+ // Exchange did not exist yet
+ oldExchangeState: undefined,
+ newExchangeState: getExchangeState(r),
+ },
+ };
}
+ return {};
}
async function provideExchangeRecordInTx(
@@ -339,7 +360,9 @@ async function provideExchangeRecordInTx(
): Promise<{
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord | undefined;
+ notification?: WalletNotification;
}> {
+ let notification: WalletNotification | undefined = undefined;
let exchange = await tx.exchanges.get(baseUrl);
if (!exchange) {
const r: ExchangeEntryRecord = {
@@ -355,15 +378,24 @@ async function provideExchangeRecordInTx(
AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
),
lastKeysEtag: undefined,
+ tosAcceptedEtag: undefined,
+ tosAcceptedTimestamp: undefined,
+ tosCurrentEtag: undefined,
};
await tx.exchanges.put(r);
exchange = r;
+ notification = {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: r.baseUrl,
+ oldExchangeState: undefined,
+ newExchangeState: getExchangeState(r),
+ };
}
const exchangeDetails = await getExchangeDetails(tx, baseUrl);
- return { exchange, exchangeDetails };
+ return { exchange, exchangeDetails, notification };
}
-interface ExchangeKeysDownloadResult {
+export interface ExchangeKeysDownloadResult {
baseUrl: string;
masterPublicKey: string;
currency: string;
@@ -393,28 +425,36 @@ async function downloadExchangeKeysInfo(
const resp = await http.fetch(keysUrl.href, {
timeout,
});
- const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeKeysJson(),
- );
- if (exchangeKeysJsonUnchecked.denominations.length === 0) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
- {
- exchangeBaseUrl: baseUrl,
- },
- "exchange doesn't offer any denominations",
- );
- }
+ // We must make sure to parse out the protocol version
+ // before we validate the body.
+ // Otherwise the parser might complain with a hard to understand
+ // message about some other field, when it is just a version
+ // incompatibility.
- const protocolVersion = exchangeKeysJsonUnchecked.version;
+ const keysJson = await resp.json();
+
+ const protocolVersion = keysJson.version;
+ if (typeof protocolVersion !== "string") {
+ throw Error("bad exchange, does not even specify protocol version");
+ }
const versionRes = LibtoolVersion.compare(
WALLET_EXCHANGE_PROTOCOL_VERSION,
protocolVersion,
);
- if (versionRes?.compatible != true) {
+ if (!versionRes) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ requestMethod: resp.requestMethod,
+ },
+ "exchange protocol version malformed",
+ );
+ }
+ if (!versionRes.compatible) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
{
@@ -425,6 +465,21 @@ async function downloadExchangeKeysInfo(
);
}
+ const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeKeysJson(),
+ );
+
+ if (exchangeKeysJsonUnchecked.denominations.length === 0) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ {
+ exchangeBaseUrl: baseUrl,
+ },
+ "exchange doesn't offer any denominations",
+ );
+ }
+
const currency = exchangeKeysJsonUnchecked.currency;
const currentDenominations: DenominationRecord[] = [];
@@ -512,7 +567,7 @@ async function downloadExchangeKeysInfo(
};
}
-export async function downloadTosFromAcceptedFormat(
+async function downloadTosFromAcceptedFormat(
ws: InternalWalletState,
baseUrl: string,
timeout: Duration,
@@ -546,54 +601,225 @@ export async function downloadTosFromAcceptedFormat(
}
/**
- * FIXME: Split this into two parts: (a) triggering the exchange
- * to be updated and (b) waiting for the update to finish.
+ * Transition an exchange into an updating state.
+ *
+ * If the update is forced, the exchange is put into an updating state
+ * even if the old information should still be up to date.
+ *
+ * For backwards compatibility, if the exchange entry doesn't exist,
+ * a new ephemeral entry is created.
*/
-export async function updateExchangeFromUrl(
+export async function startUpdateExchangeEntry(
ws: InternalWalletState,
- baseUrl: string,
- options: {
- checkMasterPub?: string;
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- } = {},
+ exchangeBaseUrl: string,
+ options: { forceUpdate?: boolean } = {},
+): Promise<void> {
+ const canonBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+
+ const now = AbsoluteTime.now();
+
+ const { notification } = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadWrite(async (tx) => {
+ return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, now);
+ });
+
+ if (notification) {
+ ws.notify(notification);
+ }
+
+ const { oldExchangeState, newExchangeState } = await ws.db
+ .mktx((x) => [x.exchanges, x.operationRetries])
+ .runReadWrite(async (tx) => {
+ const r = await tx.exchanges.get(canonBaseUrl);
+ if (!r) {
+ throw Error("exchange not found");
+ }
+ const oldExchangeState = getExchangeState(r);
+ switch (r.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ break;
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ break;
+ case ExchangeEntryDbUpdateStatus.Ready: {
+ const nextUpdateTimestamp = AbsoluteTime.fromPreciseTimestamp(
+ timestampPreciseFromDb(r.nextUpdateStamp),
+ );
+ // Only update if entry is outdated or update is forced.
+ if (
+ options.forceUpdate ||
+ AbsoluteTime.isExpired(nextUpdateTimestamp)
+ ) {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate;
+ }
+ break;
+ }
+ case ExchangeEntryDbUpdateStatus.Initial:
+ r.updateStatus = ExchangeEntryDbUpdateStatus.InitialUpdate;
+ break;
+ }
+ await tx.exchanges.put(r);
+ const newExchangeState = getExchangeState(r);
+ // Reset retries for updating the exchange entry.
+ const taskId = TaskIdentifiers.forExchangeUpdate(r);
+ await tx.operationRetries.delete(taskId);
+ return { oldExchangeState, newExchangeState };
+ });
+ ws.notify({
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: canonBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ });
+ ws.workAvailable.trigger();
+}
+
+export interface NotificationWaiter {
+ waitNext(): Promise<void>;
+ cancel(): void;
+}
+
+export function createNotificationWaiter(
+ ws: InternalWalletState,
+ pred: (x: WalletNotification) => boolean,
+): NotificationWaiter {
+ ws.ensureTaskLoopRunning();
+ let cancelFn: CancelFn | undefined = undefined;
+ let p: OpenedPromise<void> | undefined = undefined;
+
+ return {
+ cancel() {
+ cancelFn?.();
+ },
+ waitNext(): Promise<void> {
+ if (!p) {
+ p = openPromise();
+ cancelFn = ws.addNotificationListener((notif) => {
+ if (pred(notif)) {
+ // We got a notification that matches our predicate.
+ // Resolve promise for existing waiters,
+ // and create a new promise to wait for the next
+ // notification occurrence.
+ const myResolve = p?.resolve;
+ const myCancel = cancelFn;
+ p = undefined;
+ cancelFn = undefined;
+ myResolve?.();
+ myCancel?.();
+ }
+ });
+ }
+ return p.promise;
+ },
+ };
+}
+
+/**
+ * Wait until an exchange entry got successfully updated.
+ *
+ * Reject with an exception if the update encountered an error.
+ */
+export async function waitExchangeEntryUpdated(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ cancellationToken?: CancellationToken,
): Promise<{
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord;
}> {
- const canonUrl = canonicalizeBaseUrl(baseUrl);
- const res = await runTaskWithErrorReporting(
+ exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+
+ const waiter = createNotificationWaiter(
ws,
- TaskIdentifiers.forExchangeUpdateFromUrl(canonUrl),
- () => updateExchangeFromUrlHandler(ws, canonUrl, options),
+ (notif) =>
+ notif.type == NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl,
);
- switch (res.type) {
- case TaskRunResultType.Finished: {
- const now = AbsoluteTime.now();
- const { exchange, exchangeDetails } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- let exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeDetails(tx, canonUrl);
- return { exchange, exchangeDetails };
- });
- if (!exchange) {
- throw Error("exchange not found");
- }
- if (!exchangeDetails) {
- throw Error("exchange details not found");
+
+ const taskId = TaskIdentifiers.forExchangeUpdateFromUrl(exchangeBaseUrl);
+
+ while (1) {
+ const { exchange, retryRecord } = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries])
+ .runReadOnly(async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ const retryRecord = await tx.operationRetries.get(taskId);
+ return { exchange, retryRecord };
+ });
+
+ if (!exchange) {
+ throw Error("exchange does not exist anymore");
+ }
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ const details = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadOnly(async (tx) => {
+ return getExchangeDetails(tx, exchangeBaseUrl);
+ });
+ if (!details) {
+ throw Error("exchange entry inconsistent");
+ }
+ waiter.cancel();
+ return { exchange, exchangeDetails: details };
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ case ExchangeEntryDbUpdateStatus.InitialUpdate: {
+ waiter.cancel();
+ if (retryRecord?.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRecord.lastError);
+ }
+ break;
}
- return { exchange, exchangeDetails };
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ waiter.cancel();
+ if (retryRecord?.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRecord.lastError);
+ } else {
+ throw Error(
+ "updating exchange failed, error info unavailable (bug!)",
+ );
+ }
}
- case TaskRunResultType.Error:
- throw TalerError.fromUncheckedDetail(res.errorDetail);
- default:
- throw Error(`unexpected operation result (${res.type})`);
+
+ await waiter.waitNext();
}
+ throw Error("not reached");
}
/**
- * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Ensure that a fresh exchange entry exists for the given
+ * exchange base URL.
+ *
+ * The cancellation token can be used to abort waiting for the
+ * updated exchange entry.
+ *
+ * If an exchange entry for the database doesn't exist in the
+ * DB, it will be added ephemerally.
+ */
+export async function fetchFreshExchange(
+ ws: InternalWalletState,
+ baseUrl: string,
+ options: {
+ cancellationToken?: CancellationToken;
+ forceUpdate?: boolean;
+ } = {},
+): Promise<{
+ exchange: ExchangeEntryRecord;
+ exchangeDetails: ExchangeDetailsRecord;
+}> {
+ const canonUrl = canonicalizeBaseUrl(baseUrl);
+ await startUpdateExchangeEntry(ws, canonUrl, {
+ forceUpdate: options.forceUpdate,
+ });
+ return waitExchangeEntryUpdated(ws, canonUrl, options.cancellationToken);
+}
+
+/**
+ * Update an exchange entry in the wallet's database
+ * by fetching the /keys and /wire information.
* Optionally link the reserve entry to the new or existing
* exchange entry in then DB.
*/
@@ -601,48 +827,11 @@ export async function updateExchangeFromUrlHandler(
ws: InternalWalletState,
exchangeBaseUrl: string,
options: {
- checkMasterPub?: string;
- forceNow?: boolean;
cancellationToken?: CancellationToken;
} = {},
): Promise<TaskRunResult> {
- const forceNow = options.forceNow ?? false;
- logger.trace(
- `updating exchange info for ${exchangeBaseUrl}, forced: ${forceNow}`,
- );
-
- const now = AbsoluteTime.now();
+ logger.trace(`updating exchange info for ${exchangeBaseUrl}`);
exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
- let isNewExchange = true;
- const { exchange, exchangeDetails } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- let oldExch = await tx.exchanges.get(exchangeBaseUrl);
- if (oldExch) {
- isNewExchange = false;
- }
- return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, now);
- });
-
- if (
- !forceNow &&
- exchangeDetails !== undefined &&
- !AbsoluteTime.isExpired(
- AbsoluteTime.fromPreciseTimestamp(
- timestampPreciseFromDb(exchange.nextUpdateStamp),
- ),
- )
- ) {
- logger.trace("using existing exchange info");
-
- if (options.checkMasterPub) {
- if (exchangeDetails.masterPublicKey !== options.checkMasterPub) {
- throw Error(`master public key mismatch`);
- }
- }
-
- return TaskRunResult.finished();
- }
logger.trace("updating exchange /keys info");
@@ -654,12 +843,6 @@ export async function updateExchangeFromUrlHandler(
timeout,
);
- if (options.checkMasterPub) {
- if (keysInfo.masterPublicKey !== options.checkMasterPub) {
- throw Error(`master public key mismatch`);
- }
- }
-
logger.trace("validating exchange wire info");
const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
@@ -740,6 +923,7 @@ export async function updateExchangeFromUrlHandler(
logger.warn(`exchange ${exchangeBaseUrl} no longer present`);
return;
}
+ const oldExchangeState = getExchangeState(r);
const existingDetails = await getExchangeDetails(tx, r.baseUrl);
if (!existingDetails) {
detailsPointerChanged = true;
@@ -753,7 +937,6 @@ export async function updateExchangeFromUrlHandler(
}
// FIXME: We need to do some consistency checks!
}
- const existingTosAccepted = existingDetails?.tosAccepted;
const newDetails: ExchangeDetailsRecord = {
auditors: keysInfo.auditors,
currency: keysInfo.currency,
@@ -763,10 +946,9 @@ export async function updateExchangeFromUrlHandler(
globalFees,
exchangeBaseUrl: r.baseUrl,
wireInfo,
- tosCurrentEtag: tosDownload.tosEtag,
- tosAccepted: existingTosAccepted,
ageMask,
};
+ r.tosCurrentEtag = tosDownload.tosEtag;
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
@@ -787,6 +969,7 @@ export async function updateExchangeFromUrlHandler(
updateClock: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
}
+ r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
await tx.exchanges.put(r);
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(typeof drRowId.key === "number");
@@ -881,14 +1064,18 @@ export async function updateExchangeFromUrlHandler(
recoupGroupId = await ws.recoupOps.createRecoupGroup(
ws,
tx,
- exchange.baseUrl,
+ exchangeBaseUrl,
newlyRevokedCoinPubs,
);
}
+ const newExchangeState = getExchangeState(r);
+
return {
exchange: r,
exchangeDetails: newDetails,
+ oldExchangeState,
+ newExchangeState,
};
});
@@ -904,11 +1091,12 @@ export async function updateExchangeFromUrlHandler(
logger.trace("done updating exchange info in database");
- if (isNewExchange) {
- ws.notify({
- type: NotificationType.ExchangeAdded,
- });
- }
+ ws.notify({
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: updated.newExchangeState,
+ oldExchangeState: updated.oldExchangeState,
+ });
return TaskRunResult.finished();
}
@@ -926,8 +1114,8 @@ export async function getExchangePaytoUri(
): Promise<string> {
// We do the update here, since the exchange might not even exist
// yet in our database.
- const details = await getExchangeDetails
- .makeContext(ws.db)
+ const details = await ws.db
+ .mktx((x) => [x.exchangeDetails, x.exchanges])
.runReadOnly(async (tx) => {
return getExchangeDetails(tx, exchangeBaseUrl);
});
@@ -947,3 +1135,246 @@ export async function getExchangePaytoUri(
)}`,
);
}
+
+/**
+ * Get the exchange ToS in the requested format.
+ * Try to download in the accepted format not cached.
+ */
+export async function getExchangeTos(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ acceptedFormat?: string[],
+): Promise<GetExchangeTosResult> {
+ // FIXME: download ToS in acceptable format if passed!
+ const { exchange, exchangeDetails } = await fetchFreshExchange(
+ ws,
+ exchangeBaseUrl,
+ );
+
+ const tosDownload = await downloadTosFromAcceptedFormat(
+ ws,
+ exchangeBaseUrl,
+ getExchangeRequestTimeout(),
+ acceptedFormat,
+ );
+
+ await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadWrite(async (tx) => {
+ const updateExchangeEntry = await tx.exchanges.get(exchangeBaseUrl);
+ if (updateExchangeEntry) {
+ updateExchangeEntry.tosCurrentEtag = tosDownload.tosEtag;
+ await tx.exchanges.put(updateExchangeEntry);
+ }
+ });
+
+ return {
+ acceptedEtag: exchange.tosAcceptedEtag,
+ currentEtag: tosDownload.tosEtag,
+ content: tosDownload.tosText,
+ contentType: tosDownload.tosContentType,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ };
+}
+
+export interface ExchangeInfo {
+ keys: ExchangeKeysDownloadResult;
+}
+
+/**
+ * Helper function to download the exchange /keys info.
+ *
+ * Only used for testing / dbless wallet.
+ */
+export async function downloadExchangeInfo(
+ exchangeBaseUrl: string,
+ http: HttpRequestLibrary,
+): Promise<ExchangeInfo> {
+ const keysInfo = await downloadExchangeKeysInfo(
+ exchangeBaseUrl,
+ http,
+ Duration.getForever(),
+ );
+ return {
+ keys: keysInfo,
+ };
+}
+
+export async function getExchanges(
+ ws: InternalWalletState,
+): Promise<ExchangesListResponse> {
+ const exchanges: ExchangeListItem[] = [];
+ await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.exchangeDetails,
+ x.denominations,
+ x.operationRetries,
+ ])
+ .runReadOnly(async (tx) => {
+ const exchangeRecords = await tx.exchanges.iter().toArray();
+ for (const r of exchangeRecords) {
+ const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
+ const opRetryRecord = await tx.operationRetries.get(
+ TaskIdentifiers.forExchangeUpdate(r),
+ );
+ exchanges.push(
+ makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
+ );
+ }
+ });
+ return { exchanges };
+}
+
+export async function getExchangeDetailedInfo(
+ ws: InternalWalletState,
+ exchangeBaseurl: string,
+): Promise<ExchangeDetailedResponse> {
+ //TODO: should we use the forceUpdate parameter?
+ const exchange = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
+ .runReadOnly(async (tx) => {
+ const ex = await tx.exchanges.get(exchangeBaseurl);
+ const dp = ex?.detailsPointer;
+ if (!dp) {
+ return;
+ }
+ const { currency } = dp;
+ const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
+ if (!exchangeDetails) {
+ return;
+ }
+
+ const denominationRecords =
+ await tx.denominations.indexes.byExchangeBaseUrl
+ .iter(ex.baseUrl)
+ .toArray();
+
+ if (!denominationRecords) {
+ return;
+ }
+
+ const denominations: DenominationInfo[] = denominationRecords.map((x) =>
+ DenominationRecord.toDenomInfo(x),
+ );
+
+ return {
+ info: {
+ exchangeBaseUrl: ex.baseUrl,
+ currency,
+ paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
+ auditors: exchangeDetails.auditors,
+ wireInfo: exchangeDetails.wireInfo,
+ globalFees: exchangeDetails.globalFees,
+ },
+ denominations,
+ };
+ });
+
+ if (!exchange) {
+ throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
+ }
+
+ const denoms = exchange.denominations.map((d) => ({
+ ...d,
+ group: Amounts.stringifyValue(d.value),
+ }));
+ const denomFees: DenomOperationMap<FeeDescription[]> = {
+ deposit: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireDeposit",
+ "feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ refresh: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeRefresh",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ refund: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeRefund",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ withdraw: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeWithdraw",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ };
+
+ const transferFees = Object.entries(
+ exchange.info.wireInfo.feesForType,
+ ).reduce((prev, [wireType, infoForType]) => {
+ const feesByGroup = [
+ ...infoForType.map((w) => ({
+ ...w,
+ fee: Amounts.stringify(w.closingFee),
+ group: "closing",
+ })),
+ ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
+ ];
+ prev[wireType] = createTimeline(
+ feesByGroup,
+ "sig",
+ "startStamp",
+ "endStamp",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+ return prev;
+ }, {} as Record<string, FeeDescription[]>);
+
+ const globalFeesByGroup = [
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.accountFee,
+ group: "account",
+ })),
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.historyFee,
+ group: "history",
+ })),
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.purseFee,
+ group: "purse",
+ })),
+ ];
+
+ const globalFees = createTimeline(
+ globalFeesByGroup,
+ "signature",
+ "startDate",
+ "endDate",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+
+ return {
+ exchange: {
+ ...exchange.info,
+ denomFees,
+ transferFees,
+ globalFees,
+ },
+ };
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
index 44c9436b1..f8ab07b10 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -63,7 +63,7 @@ import {
timestampOptionalPreciseFromDb,
timestampPreciseFromDb,
timestampPreciseToDb,
- updateExchangeFromUrl,
+ fetchFreshExchange,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
@@ -764,7 +764,7 @@ export async function initiatePeerPullPayment(
const exchangeBaseUrl = maybeExchangeBaseUrl;
- await updateExchangeFromUrl(ws, exchangeBaseUrl);
+ await fetchFreshExchange(ws, exchangeBaseUrl);
const mergeReserveInfo = await getMergeReserveInfo(ws, {
exchangeBaseUrl: exchangeBaseUrl,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index 690edf2e7..575780ba4 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -69,7 +69,7 @@ import {
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import { fetchFreshExchange } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
@@ -141,7 +141,7 @@ export async function preparePeerPushCredit(
const exchangeBaseUrl = uri.exchangeBaseUrl;
- await updateExchangeFromUrl(ws, exchangeBaseUrl);
+ await fetchFreshExchange(ws, exchangeBaseUrl);
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 282f84ad7..a9d6c5595 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -22,12 +22,17 @@
* Imports.
*/
import { GlobalIDB } from "@gnu-taler/idb-bridge";
-import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TransactionRecordFilter,
+} from "@gnu-taler/taler-util";
import {
BackupProviderStateTag,
+ DbPreciseTimestamp,
DepositElementStatus,
DepositGroupRecord,
- DepositOperationStatus,
ExchangeEntryDbUpdateStatus,
PeerPullCreditRecord,
PeerPullDebitRecordStatus,
@@ -48,7 +53,6 @@ import {
RewardRecordStatus,
WalletStoresV1,
WithdrawalGroupRecord,
- WithdrawalGroupStatus,
depositOperationNonfinalStatusRange,
timestampAbsoluteFromDb,
timestampOptionalAbsoluteFromDb,
@@ -94,18 +98,29 @@ async function gatherExchangePending(
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
- // FIXME: We should do a range query here based on the update time
- // and/or the entry state.
+ let timestampDue: DbPreciseTimestamp | undefined = undefined;
await tx.exchanges.iter().forEachAsync(async (exch) => {
switch (exch.updateStatus) {
case ExchangeEntryDbUpdateStatus.Initial:
case ExchangeEntryDbUpdateStatus.Suspended:
- case ExchangeEntryDbUpdateStatus.Failed:
return;
}
const opUpdateExchangeTag = TaskIdentifiers.forExchangeUpdate(exch);
let opr = await tx.operationRetries.get(opUpdateExchangeTag);
- const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp;
+
+ switch (exch.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ case ExchangeEntryDbUpdateStatus.InitialUpdate:
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ timestampDue =
+ opr?.retryInfo.nextRetry ??
+ timestampPreciseToDb(TalerPreciseTimestamp.now());
+ break;
+ }
+
resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate,
...getPendingCommon(
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 3afdd2d71..51dd9adac 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -98,7 +98,11 @@ import {
TaskRunResult,
TaskRunResultType,
} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import {
+ fetchFreshExchange,
+ startUpdateExchangeEntry,
+ waitExchangeEntryUpdated,
+} from "./exchanges.js";
import {
constructTransactionIdentifier,
notifyTransition,
@@ -221,7 +225,7 @@ async function provideRefreshSession(
const { refreshGroup, coin } = d;
- const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
+ const { exchange } = await fetchFreshExchange(ws, coin.exchangeBaseUrl);
if (!exchange) {
throw Error("db inconsistent: exchange of coin not found");
}
@@ -1157,9 +1161,7 @@ export async function autoRefresh(
// We must make sure that the exchange is up-to-date so that
// can refresh into new denominations.
- await updateExchangeFromUrl(ws, exchangeBaseUrl, {
- forceNow: true,
- });
+ await fetchFreshExchange(ws, exchangeBaseUrl);
let minCheckThreshold = AbsoluteTime.addDuration(
AbsoluteTime.now(),
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 5d609f41d..90320d7cb 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -69,7 +69,7 @@ import {
TaskRunResult,
TaskRunResultType,
} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import { fetchFreshExchange } from "./exchanges.js";
import {
getCandidateWithdrawalDenoms,
getExchangeWithdrawalInfo,
@@ -175,7 +175,7 @@ export async function prepareTip(
const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount);
logger.trace("new tip, creating tip record");
- await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
+ await fetchFreshExchange(ws, tipPickupStatus.exchange_url);
//FIXME: is this needed? withdrawDetails is not used
// * if the intention is to update the exchange information in the database
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index b30c5f80b..a03d54d3a 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -58,7 +58,7 @@ import { OpenedPromise, openPromise } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { getBalances } from "./balance.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import { fetchFreshExchange } from "./exchanges.js";
import {
confirmPay,
preparePayForUri,
@@ -579,7 +579,7 @@ export async function runIntegrationTest2(
// waiting for notifications.
logger.info("running test with arguments", args);
- const exchangeInfo = await updateExchangeFromUrl(ws, args.exchangeBaseUrl);
+ const exchangeInfo = await fetchFreshExchange(ws, args.exchangeBaseUrl);
const currency = exchangeInfo.exchangeDetails.currency;
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index b9ba3058f..e7ba6d820 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -135,7 +135,7 @@ import {
import {
getExchangeDetails,
getExchangePaytoUri,
- updateExchangeFromUrl,
+ fetchFreshExchange,
} from "./exchanges.js";
import {
TransitionInfo,
@@ -1862,8 +1862,8 @@ export async function getExchangeWithdrawalInfo(
}
let tosAccepted = false;
- if (exchangeDetails.tosAccepted?.timestamp) {
- if (exchangeDetails.tosAccepted.etag === exchangeDetails.tosCurrentEtag) {
+ if (exchange.tosAcceptedTimestamp) {
+ if (exchange.tosAcceptedEtag === exchange.tosCurrentEtag) {
tosAccepted = true;
}
}
@@ -2372,7 +2372,7 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
+ const exchangeInfo = await fetchFreshExchange(ws, canonExchange);
const exchangeDetails = exchangeInfo.exchangeDetails;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
@@ -2515,7 +2515,7 @@ export async function acceptWithdrawalFromUri(
};
}
- await updateExchangeFromUrl(ws, selectedExchange);
+ await fetchFreshExchange(ws, selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(
ws.http,
req.talerWithdrawUri,
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 25cfd7f6f..c9612da5f 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -33,17 +33,10 @@ import {
CoreApiResponse,
CreateStoredBackupResponse,
DeleteStoredBackupRequest,
- DenomOperationMap,
DenominationInfo,
Duration,
- ExchangeDetailedResponse,
- ExchangeListItem,
- ExchangesListResponse,
ExchangesShortListResponse,
- FeeDescription,
GetCurrencySpecificationResponse,
- GetExchangeEntryByUrlResponse,
- GetExchangeTosResult,
InitResponse,
KnownBankAccounts,
KnownBankAccountsInfo,
@@ -194,7 +187,6 @@ import { getBalanceDetail, getBalances } from "./operations/balance.js";
import {
TaskIdentifiers,
TaskRunResult,
- getExchangeTosStatus,
makeExchangeListItem,
runTaskWithErrorReporting,
} from "./operations/common.js";
@@ -208,10 +200,11 @@ import {
import {
acceptExchangeTermsOfService,
addPresetExchangeEntry,
- downloadTosFromAcceptedFormat,
+ fetchFreshExchange,
+ getExchangeDetailedInfo,
getExchangeDetails,
- getExchangeRequestTimeout,
- updateExchangeFromUrl,
+ getExchangeTos,
+ getExchanges,
updateExchangeFromUrlHandler,
} from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js";
@@ -296,11 +289,6 @@ import {
import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js";
import {
- createTimeline,
- selectBestForOverlappingDenominations,
- selectMinimumFee,
-} from "./util/denominations.js";
-import {
convertDepositAmount,
convertPeerPushAmount,
convertWithdrawalAmount,
@@ -520,12 +508,13 @@ async function runTaskLoop(
continue;
}
logger.trace(`running task ${p.id}`);
- await runTaskWithErrorReporting(ws, p.id, async () => {
+ const res = await runTaskWithErrorReporting(ws, p.id, async () => {
return await callOperationHandler(ws, p);
});
ws.notify({
type: NotificationType.PendingOperationProcessed,
id: p.id,
+ taskResultType: res.type,
});
if (ws.stopped) {
ws.isTaskLoopRunning = false;
@@ -549,6 +538,7 @@ async function runTaskLoop(
* already been applied.
*/
async function fillDefaults(ws: InternalWalletState): Promise<void> {
+ const notifications: WalletNotification[] = [];
await ws.db
.mktx((x) => [x.config, x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => {
@@ -559,55 +549,23 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
return;
}
for (const exch of ws.config.builtin.exchanges) {
- await addPresetExchangeEntry(
+ const resp = await addPresetExchangeEntry(
tx,
exch.exchangeBaseUrl,
exch.currencyHint,
);
+ if (resp.notification) {
+ notifications.push(resp.notification);
+ }
}
await tx.config.put({
key: ConfigRecordKey.CurrencyDefaultsApplied,
value: true,
});
});
-}
-
-/**
- * Get the exchange ToS in the requested format.
- * Try to download in the accepted format not cached.
- */
-async function getExchangeTos(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- acceptedFormat?: string[],
-): Promise<GetExchangeTosResult> {
- // FIXME: download ToS in acceptable format if passed!
- const { exchangeDetails } = await updateExchangeFromUrl(ws, exchangeBaseUrl);
-
- const tosDownload = await downloadTosFromAcceptedFormat(
- ws,
- exchangeBaseUrl,
- getExchangeRequestTimeout(),
- acceptedFormat,
- );
-
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- const d = await getExchangeDetails(tx, exchangeBaseUrl);
- if (d) {
- d.tosCurrentEtag = tosDownload.tosEtag;
- await tx.exchangeDetails.put(d);
- }
- });
-
- return {
- acceptedEtag: exchangeDetails.tosAccepted?.etag,
- currentEtag: tosDownload.tosEtag,
- content: tosDownload.tosText,
- contentType: tosDownload.tosContentType,
- tosStatus: getExchangeTosStatus(exchangeDetails),
- };
+ for (const notif of notifications) {
+ ws.notify(notif);
+ }
}
/**
@@ -680,185 +638,6 @@ async function forgetKnownBankAccounts(
return;
}
-async function getExchanges(
- ws: InternalWalletState,
-): Promise<ExchangesListResponse> {
- const exchanges: ExchangeListItem[] = [];
- await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.operationRetries,
- ])
- .runReadOnly(async (tx) => {
- const exchangeRecords = await tx.exchanges.iter().toArray();
- for (const r of exchangeRecords) {
- const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
- const opRetryRecord = await tx.operationRetries.get(
- TaskIdentifiers.forExchangeUpdate(r),
- );
- exchanges.push(
- makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
- );
- }
- });
- return { exchanges };
-}
-
-async function getExchangeDetailedInfo(
- ws: InternalWalletState,
- exchangeBaseurl: string,
-): Promise<ExchangeDetailedResponse> {
- //TODO: should we use the forceUpdate parameter?
- const exchange = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
- .runReadOnly(async (tx) => {
- const ex = await tx.exchanges.get(exchangeBaseurl);
- const dp = ex?.detailsPointer;
- if (!dp) {
- return;
- }
- const { currency } = dp;
- const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
- if (!exchangeDetails) {
- return;
- }
-
- const denominationRecords =
- await tx.denominations.indexes.byExchangeBaseUrl
- .iter(ex.baseUrl)
- .toArray();
-
- if (!denominationRecords) {
- return;
- }
-
- const denominations: DenominationInfo[] = denominationRecords.map((x) =>
- DenominationRecord.toDenomInfo(x),
- );
-
- return {
- info: {
- exchangeBaseUrl: ex.baseUrl,
- currency,
- paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
- auditors: exchangeDetails.auditors,
- wireInfo: exchangeDetails.wireInfo,
- globalFees: exchangeDetails.globalFees,
- },
- denominations,
- };
- });
-
- if (!exchange) {
- throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
- }
-
- const denoms = exchange.denominations.map((d) => ({
- ...d,
- group: Amounts.stringifyValue(d.value),
- }));
- const denomFees: DenomOperationMap<FeeDescription[]> = {
- deposit: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireDeposit",
- "feeDeposit",
- "group",
- selectBestForOverlappingDenominations,
- ),
- refresh: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireWithdraw",
- "feeRefresh",
- "group",
- selectBestForOverlappingDenominations,
- ),
- refund: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireWithdraw",
- "feeRefund",
- "group",
- selectBestForOverlappingDenominations,
- ),
- withdraw: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireWithdraw",
- "feeWithdraw",
- "group",
- selectBestForOverlappingDenominations,
- ),
- };
-
- const transferFees = Object.entries(
- exchange.info.wireInfo.feesForType,
- ).reduce((prev, [wireType, infoForType]) => {
- const feesByGroup = [
- ...infoForType.map((w) => ({
- ...w,
- fee: Amounts.stringify(w.closingFee),
- group: "closing",
- })),
- ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
- ];
- prev[wireType] = createTimeline(
- feesByGroup,
- "sig",
- "startStamp",
- "endStamp",
- "fee",
- "group",
- selectMinimumFee,
- );
- return prev;
- }, {} as Record<string, FeeDescription[]>);
-
- const globalFeesByGroup = [
- ...exchange.info.globalFees.map((w) => ({
- ...w,
- fee: w.accountFee,
- group: "account",
- })),
- ...exchange.info.globalFees.map((w) => ({
- ...w,
- fee: w.historyFee,
- group: "history",
- })),
- ...exchange.info.globalFees.map((w) => ({
- ...w,
- fee: w.purseFee,
- group: "purse",
- })),
- ];
-
- const globalFees = createTimeline(
- globalFeesByGroup,
- "signature",
- "startDate",
- "endDate",
- "fee",
- "group",
- selectMinimumFee,
- );
-
- return {
- exchange: {
- ...exchange.info,
- denomFees,
- transferFees,
- globalFees,
- },
- };
-}
-
async function setCoinSuspended(
ws: InternalWalletState,
coinPub: string,
@@ -1059,7 +838,7 @@ async function handlePrepareWithdrawExchange(
throw Error("expected a taler://withdraw-exchange URI");
}
const exchangeBaseUrl = parsedUri.exchangeBaseUrl;
- const exchange = await updateExchangeFromUrl(ws, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(ws, exchangeBaseUrl);
if (exchange.exchangeDetails.masterPublicKey != parsedUri.exchangePub) {
throw Error("mismatch of exchange master public key (URI vs actual)");
}
@@ -1166,15 +945,14 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.AddExchange: {
const req = codecForAddExchangeRequest().decode(payload);
- await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {
- checkMasterPub: req.masterPub,
- forceNow: req.forceUpdate,
- });
+ await fetchFreshExchange(ws, req.exchangeBaseUrl);
return {};
}
case WalletApiOperation.UpdateExchangeEntry: {
const req = codecForUpdateExchangeEntryRequest().decode(payload);
- await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {});
+ await fetchFreshExchange(ws, req.exchangeBaseUrl, {
+ forceUpdate: true,
+ });
return {};
}
case WalletApiOperation.ListExchanges: {
@@ -1896,7 +1674,7 @@ class InternalWalletStateImpl implements InternalWalletState {
exchangeOps: ExchangeOperations = {
getExchangeDetails,
- updateExchangeFromUrl,
+ updateExchangeFromUrl: fetchFreshExchange,
};
recoupOps: RecoupOperations = {