From d19aef746c1e67deaccc7c8cefba008f2f0d46ca Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 30 Aug 2023 15:54:56 +0200 Subject: [PATCH] wallet-core: towards DD48 --- packages/taler-util/src/time.ts | 22 +++++ packages/taler-util/src/wallet-types.ts | 34 ++++---- .../src/internal-wallet-state.ts | 6 +- .../src/operations/backup/import.ts | 27 +++---- .../src/operations/common.ts | 80 +++++++++++++------ .../src/operations/exchanges.ts | 56 ++++++++++--- .../src/operations/pending.ts | 28 ++++--- .../src/operations/refresh.ts | 10 +-- .../src/operations/withdraw.ts | 7 +- packages/taler-wallet-core/src/wallet.ts | 5 +- 10 files changed, 182 insertions(+), 93 deletions(-) diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 75b3c7e94..55cda08a5 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -312,6 +312,14 @@ export namespace Duration { } export namespace AbsoluteTime { + export function getStampMsNow(): number { + return new Date().getTime(); + } + + export function getStampMsNever(): number { + return Number.MAX_SAFE_INTEGER; + } + export function now(): AbsoluteTime { return { t_ms: new Date().getTime() + timeshift, @@ -398,6 +406,13 @@ export namespace AbsoluteTime { }; } + export function fromStampMs(stampMs: number): AbsoluteTime { + return { + t_ms: stampMs, + [opaque_AbsoluteTime]: true, + }; + } + export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime { if (t.t_s === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; @@ -409,6 +424,13 @@ export namespace AbsoluteTime { }; } + export function toStampMs(at: AbsoluteTime): number { + if (at.t_ms === "never") { + return Number.MAX_SAFE_INTEGER; + } + return at.t_ms; + } + export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp { if (at.t_ms == "never") { return { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 04fb43ec6..01c1838d5 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1262,17 +1262,25 @@ export interface ExchangeFullDetails { } export enum ExchangeTosStatus { - New = "new", + Pending = "pending", + Proposed = "proposed", Accepted = "accepted", - Changed = "changed", - NotFound = "not-found", - Unknown = "unknown", } export enum ExchangeEntryStatus { - Unknown = "unknown", - Outdated = "outdated", - Ok = "ok", + Preset = "preset", + Ephemeral = "ephemeral", + Used = "used", +} + +export enum ExchangeUpdateStatus { + Initial = "initial", + InitialUpdate = "initial(update)", + Suspended = "suspended", + Failed = "failed", + OutdatedUpdate = "outdated(update)", + Ready = "ready", + ReadyUpdate = "ready(update)", } export interface OperationErrorInfo { @@ -1285,13 +1293,9 @@ export interface ExchangeListItem { currency: string | undefined; paytoUris: string[]; tosStatus: ExchangeTosStatus; - exchangeStatus: ExchangeEntryStatus; + exchangeEntryStatus: ExchangeEntryStatus; + exchangeUpdateStatus: ExchangeUpdateStatus; ageRestrictionOptions: number[]; - /** - * Permanently added to the wallet, as opposed to just - * temporarily queried. - */ - permanent: boolean; /** * Information about the last error that occurred when trying @@ -1370,8 +1374,8 @@ export const codecForExchangeListItem = (): Codec => .property("exchangeBaseUrl", codecForString()) .property("paytoUris", codecForList(codecForString())) .property("tosStatus", codecForAny()) - .property("exchangeStatus", codecForAny()) - .property("permanent", codecForBoolean()) + .property("exchangeEntryStatus", codecForAny()) + .property("exchangeUpdateStatus", codecForAny()) .property("ageRestrictionOptions", codecForList(codecForNumber())) .build("ExchangeListItem"); diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 742af89a8..76aee05bd 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -42,7 +42,7 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, RefreshReasonDetails, WalletStoresV1, } from "./db.js"; @@ -108,7 +108,7 @@ export interface ExchangeOperations { ): Promise; getExchangeTrust( ws: InternalWalletState, - exchangeInfo: ExchangeRecord, + exchangeInfo: ExchangeEntryRecord, ): Promise; updateExchangeFromUrl( ws: InternalWalletState, @@ -118,7 +118,7 @@ export interface ExchangeOperations { cancellationToken?: CancellationToken; }, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord; }>; } diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index a53b624e8..836c65643 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -342,20 +342,19 @@ export async function importBackup( if (existingExchange) { continue; } - await tx.exchanges.put({ - baseUrl: backupExchange.base_url, - detailsPointer: { - currency: backupExchange.currency, - masterPublicKey: backupExchange.master_public_key, - updateClock: backupExchange.update_clock, - }, - permanent: true, - lastUpdate: undefined, - nextUpdate: TalerPreciseTimestamp.now(), - nextRefreshCheck: TalerPreciseTimestamp.now(), - lastKeysEtag: undefined, - lastWireEtag: undefined, - }); + // await tx.exchanges.put({ + // baseUrl: backupExchange.base_url, + // detailsPointer: { + // currency: backupExchange.currency, + // masterPublicKey: backupExchange.master_public_key, + // updateClock: backupExchange.update_clock, + // }, + // lastUpdate: undefined, + // nextUpdate: TalerPreciseTimestamp.now(), + // nextRefreshCheck: TalerPreciseTimestamp.now(), + // lastKeysEtag: undefined, + // lastWireEtag: undefined, + // }); } for (const backupExchangeDetails of backupBlob.exchange_details) { diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 7a8b78b53..e96beb5b2 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -30,6 +30,7 @@ import { ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus, + ExchangeUpdateStatus, getErrorDetailFromException, j2s, Logger, @@ -47,7 +48,7 @@ import { WalletStoresV1, CoinRecord, ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, BackupProviderRecord, DepositGroupRecord, PeerPullPaymentIncomingRecord, @@ -59,6 +60,8 @@ import { RefreshGroupRecord, RewardRecord, WithdrawalGroupRecord, + ExchangeEntryDbUpdateStatus, + ExchangeEntryDbRecordStatus, } from "../db.js"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -529,16 +532,16 @@ export function getExchangeTosStatus( exchangeDetails: ExchangeDetailsRecord, ): ExchangeTosStatus { if (!exchangeDetails.tosAccepted) { - return ExchangeTosStatus.New; + return ExchangeTosStatus.Proposed; } if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) { return ExchangeTosStatus.Accepted; } - return ExchangeTosStatus.Changed; + return ExchangeTosStatus.Proposed; } export function makeExchangeListItem( - r: ExchangeRecord, + r: ExchangeEntryRecord, exchangeDetails: ExchangeDetailsRecord | undefined, lastError: TalerErrorDetail | undefined, ): ExchangeListItem { @@ -547,30 +550,57 @@ export function makeExchangeListItem( error: lastError, } : undefined; - if (!exchangeDetails) { - return { - exchangeBaseUrl: r.baseUrl, - currency: undefined, - tosStatus: ExchangeTosStatus.Unknown, - paytoUris: [], - exchangeStatus: ExchangeEntryStatus.Unknown, - permanent: r.permanent, - ageRestrictionOptions: [], - lastUpdateErrorInfo, - }; + + let exchangeUpdateStatus: ExchangeUpdateStatus; + switch (r.updateStatus) { + case ExchangeEntryDbUpdateStatus.Failed: + exchangeUpdateStatus = ExchangeUpdateStatus.Failed; + break; + case ExchangeEntryDbUpdateStatus.Initial: + exchangeUpdateStatus = ExchangeUpdateStatus.Initial; + break; + case ExchangeEntryDbUpdateStatus.InitialUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate; + break; + case ExchangeEntryDbUpdateStatus.OutdatedUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate; + break; + case ExchangeEntryDbUpdateStatus.Ready: + exchangeUpdateStatus = ExchangeUpdateStatus.Ready; + break; + case ExchangeEntryDbUpdateStatus.ReadyUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate; + break; + case ExchangeEntryDbUpdateStatus.Suspended: + exchangeUpdateStatus = ExchangeUpdateStatus.Suspended; + break; } - let exchangeStatus; - exchangeStatus = ExchangeEntryStatus.Ok; + + let exchangeEntryStatus: ExchangeEntryStatus; + switch (r.entryStatus) { + case ExchangeEntryDbRecordStatus.Ephemeral: + exchangeEntryStatus = ExchangeEntryStatus.Ephemeral; + break; + case ExchangeEntryDbRecordStatus.Preset: + exchangeEntryStatus = ExchangeEntryStatus.Preset; + break; + case ExchangeEntryDbRecordStatus.Used: + exchangeEntryStatus = ExchangeEntryStatus.Used; + break; + } + return { exchangeBaseUrl: r.baseUrl, - currency: exchangeDetails.currency, - tosStatus: getExchangeTosStatus(exchangeDetails), - paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), - exchangeStatus, - permanent: r.permanent, - ageRestrictionOptions: exchangeDetails.ageMask + currency: exchangeDetails?.currency, + exchangeUpdateStatus, + exchangeEntryStatus, + tosStatus: exchangeDetails + ? getExchangeTosStatus(exchangeDetails) + : ExchangeTosStatus.Pending, + ageRestrictionOptions: exchangeDetails?.ageMask ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask) : [], + paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [], lastUpdateErrorInfo, }; } @@ -892,13 +922,13 @@ export namespace TaskIdentifiers { export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId { return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId; } - export function forExchangeUpdate(exch: ExchangeRecord): TaskId { + export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId { return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId; } export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId { return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId; } - export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { + export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId { return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId; } export function forTipPickup(tipRecord: RewardRecord): TaskId { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index c6b46e360..311a71a6e 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import { encodeCrock, ExchangeAuditor, ExchangeDenomination, + ExchangeEntryStatus, ExchangeGlobalFees, ExchangeSignKeyJson, ExchangeWireJson, @@ -66,10 +67,15 @@ import { DenominationRecord, DenominationVerificationStatus, ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, WalletStoresV1, } from "../db.js"; -import { isWithdrawableDenom } from "../index.js"; +import { + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, + isWithdrawableDenom, + WalletDbReadWriteTransaction, +} from "../index.js"; import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; import { @@ -326,6 +332,26 @@ export async function downloadExchangeInfo( }; } +export async function addPresetExchangeEntry( + tx: WalletDbReadWriteTransaction<"exchanges">, + exchangeBaseUrl: string, +): Promise { + let exchange = await tx.exchanges.get(exchangeBaseUrl); + if (!exchange) { + const r: ExchangeEntryRecord = { + entryStatus: ExchangeEntryDbRecordStatus.Preset, + updateStatus: ExchangeEntryDbUpdateStatus.Initial, + baseUrl: exchangeBaseUrl, + detailsPointer: undefined, + lastUpdate: undefined, + lastKeysEtag: undefined, + nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), + nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + }; + await tx.exchanges.put(r); + } +} + export async function provideExchangeRecordInTx( ws: InternalWalletState, tx: GetReadWriteAccess<{ @@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx( baseUrl: string, now: AbsoluteTime, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord | undefined; }> { let exchange = await tx.exchanges.get(baseUrl); if (!exchange) { - const r: ExchangeRecord = { - permanent: true, + const r: ExchangeEntryRecord = { + entryStatus: ExchangeEntryDbRecordStatus.Ephemeral, + updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate, baseUrl: baseUrl, detailsPointer: undefined, lastUpdate: undefined, - nextUpdate: AbsoluteTime.toPreciseTimestamp(now), - nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now), + nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), lastKeysEtag: undefined, - lastWireEtag: undefined, }; await tx.exchanges.put(r); exchange = r; @@ -534,6 +560,10 @@ 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. + */ export async function updateExchangeFromUrl( ws: InternalWalletState, baseUrl: string, @@ -543,7 +573,7 @@ export async function updateExchangeFromUrl( cancellationToken?: CancellationToken; } = {}, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord; }> { const canonUrl = canonicalizeBaseUrl(baseUrl); @@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler( !forceNow && exchangeDetails !== undefined && !AbsoluteTime.isExpired( - AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate), + AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs), ) ) { logger.trace("using existing exchange info"); @@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler( newDetails.rowId = existingDetails.rowId; } r.lastUpdate = TalerPreciseTimestamp.now(); - r.nextUpdate = AbsoluteTime.toPreciseTimestamp( + r.nextUpdateStampMs = AbsoluteTime.toStampMs( AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry), ); // New denominations might be available. - r.nextRefreshCheck = TalerPreciseTimestamp.now(); + r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow(); if (detailsPointerChanged) { r.detailsPointer = { currency: newDetails.currency, @@ -948,7 +978,7 @@ export async function getExchangePaytoUri( */ export async function getExchangeTrust( ws: InternalWalletState, - exchangeInfo: ExchangeRecord, + exchangeInfo: ExchangeEntryRecord, ): Promise { let isTrusted = false; let isAudited = false; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 6c6546f83..e37e45c16 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -45,6 +45,7 @@ import { PeerPushPaymentIncomingRecord, RefundGroupRecord, RefundGroupStatus, + ExchangeEntryDbUpdateStatus, } from "../db.js"; import { PendingOperationsResponse, @@ -81,19 +82,25 @@ async function gatherExchangePending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { - // FIXME: We should do a range query here based on the update time. + // FIXME: We should do a range query here based on the update time + // and/or the entry state. await tx.exchanges.iter().forEachAsync(async (exch) => { + switch (exch.updateStatus) { + case ExchangeEntryDbUpdateStatus.Initial: + case ExchangeEntryDbUpdateStatus.Suspended: + case ExchangeEntryDbUpdateStatus.Failed: + return; + } const opTag = TaskIdentifiers.forExchangeUpdate(exch); let opr = await tx.operationRetries.get(opTag); const timestampDue = opr?.retryInfo.nextRetry ?? - AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate); + AbsoluteTime.fromStampMs(exch.nextUpdateStampMs); resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, ...getPendingCommon(ws, opTag, timestampDue), @@ -108,7 +115,7 @@ async function gatherExchangePending( resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, ...getPendingCommon(ws, opTag, timestampDue), - timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck), + timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs), givesLifeness: false, exchangeBaseUrl: exch.baseUrl, }); @@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal( WithdrawalGroupStatus.PendingRegisteringBank, WithdrawalGroupStatus.PendingAml, ); - withdrawalGroupRecords = - await tx.withdrawalGroups.indexes.byStatus.getAll(range); + withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll( + range, + ); } else { withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(); @@ -344,12 +352,8 @@ export async function iterRecordsForRefund( f: (r: RefundGroupRecord) => Promise, ): Promise { if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.only( - RefundGroupStatus.Pending - ); - await tx.refundGroups.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending); + await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.refundGroups.iter().forEachAsync(f); } diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 72d1a2725..fb356f0fc 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -1190,14 +1190,14 @@ export async function autoRefresh( `created refresh group for auto-refresh (${res.refreshGroupId})`, ); } -// logger.trace( -// `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, -// ); + // logger.trace( + // `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, + // ); logger.trace( `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`, ); - exchange.nextRefreshCheck = - AbsoluteTime.toPreciseTimestamp(minCheckThreshold); + exchange.nextRefreshCheckStampMs = + AbsoluteTime.toStampMs(minCheckThreshold); await tx.exchanges.put(exchange); }); return TaskRunResult.finished(); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 040d191e1..d8ce0a9a2 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -128,6 +128,8 @@ import { } from "../util/coinSelection.js"; import { ExchangeDetailsRecord, + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, PendingTaskType, isWithdrawableDenom, } from "../index.js"; @@ -2341,10 +2343,6 @@ export async function internalPerformCreateWithdrawalGroup( }>, prep: PrepareCreateWithdrawalGroupResult, ): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId, - }); const { withdrawalGroup } = prep; if (!prep.creationInfo) { return { withdrawalGroup, transitionInfo: undefined }; @@ -2361,6 +2359,7 @@ export async function internalPerformCreateWithdrawalGroup( const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); if (exchange) { exchange.lastWithdrawal = TalerPreciseTimestamp.now(); + exchange.entryStatus = ExchangeEntryDbRecordStatus.Used; await tx.exchanges.put(exchange); } diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 194894e52..c9ccda20d 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -189,6 +189,7 @@ import { } from "./operations/deposits.js"; import { acceptExchangeTermsOfService, + addPresetExchangeEntry, downloadTosFromAcceptedFormat, getExchangeDetails, getExchangeRequestTimeout, @@ -533,6 +534,7 @@ async function fillDefaults(ws: InternalWalletState): Promise { await tx.auditorTrust.put(c); } for (const baseUrl of ws.config.builtin.exchanges) { + await addPresetExchangeEntry(tx, baseUrl); const now = AbsoluteTime.now(); provideExchangeRecordInTx(ws, tx, baseUrl, now); } @@ -1688,8 +1690,7 @@ export class Wallet { public static defaultConfig: Readonly = { builtin: { - //exchanges: ["https://exchange.demo.taler.net/"], - exchanges: [], + exchanges: ["https://exchange.demo.taler.net/"], auditors: [ { currency: "KUDOS",