taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 4b5367e23483965f83d9ea58528f654a583ba41a
parent 0b248fb5dc463e965385a762d525a70d616dfae0
Author: Florian Dold <florian@dold.me>
Date:   Fri, 29 May 2026 19:38:38 +0200

wallet-core: simplify retry handling for exchange updates

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-denom-lost.ts | 15++++++++++-----
Mpackages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts | 15++++++++++-----
Mpackages/taler-harness/src/integrationtests/test-wallet-devexp-fakeprotover.ts | 15++++++++++-----
Mpackages/taler-harness/src/integrationtests/test-wallet-exchange-migration-existing.ts | 9+++++++--
Mpackages/taler-harness/src/integrationtests/test-wallet-exchange-migration.ts | 9+++++++--
Mpackages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts | 17+++++++++++++++--
Mpackages/taler-util/src/types-taler-wallet.ts | 21+++++++++++++++++++++
Mpackages/taler-wallet-core/src/db.ts | 13++-----------
Mpackages/taler-wallet-core/src/exchanges.ts | 282+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 23++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/wallet.ts | 25++++++++++++++++++++++++-
11 files changed, 276 insertions(+), 168 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-denom-lost.ts b/packages/taler-harness/src/integrationtests/test-denom-lost.ts @@ -18,11 +18,11 @@ * Imports. */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV3, - withdrawViaBankV3, + withdrawViaBankV4, } from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; /** * Run test for refreshe after a payment. @@ -30,14 +30,14 @@ import { export async function runDenomLostTest(t: GlobalTestState) { // Set up test environment - const { walletClient, bankClient, exchange } = + const { bank, walletClient, exchange } = await createSimpleTestkudosEnvironmentV3(t); // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV3(t, { + const wres = await withdrawViaBankV4(t, { walletClient, - bankClient, + bank, exchange, amount: "TESTKUDOS:20", }); @@ -65,6 +65,11 @@ export async function runDenomLostTest(t: GlobalTestState) { force: true, }); + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { + exchangeBaseUrl: exchange.baseUrl, + forceUpdate: true, + }); + const dsAfter = await walletClient.call( WalletApiOperation.TestingGetDenomStats, { diff --git a/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts b/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts @@ -85,12 +85,17 @@ export async function runExchangeMasterPubChangeTest( t.logStep("exchange-restarted"); - const err = await t.assertThrowsTalerErrorAsync(async () => - walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchange.baseUrl, + force: true, + }); + + const err = await t.assertThrowsTalerErrorAsync(async () => { + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { exchangeBaseUrl: exchange.baseUrl, - force: true, - }), - ); + forceUpdate: true, + }); + }); console.log("updateExchangeEntry err:", j2s(err)); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-devexp-fakeprotover.ts b/packages/taler-harness/src/integrationtests/test-wallet-devexp-fakeprotover.ts @@ -76,12 +76,17 @@ export async function runWalletDevexpFakeprotoverTest(t: GlobalTestState) { logger.info("updating exchange entry after dev experiment"); - const err1 = await t.assertThrowsTalerErrorAsync(async () => - walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchange.baseUrl, + force: true, + }); + + const err1 = await t.assertThrowsTalerErrorAsync(async () => { + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { exchangeBaseUrl: exchange.baseUrl, - force: true, - }), - ); + forceUpdate: true, + }); + }); t.assertTrue( err1.errorDetail.code === TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-migration-existing.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-migration-existing.ts @@ -90,10 +90,15 @@ export async function runWalletExchangeMigrationExistingTest( }, ); + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchange.baseUrl, + force: true, + }); + try { - await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { exchangeBaseUrl: exchange.baseUrl, - force: true, + forceUpdate: true, }); } catch (e) {} diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-migration.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-migration.ts @@ -76,10 +76,15 @@ export async function runWalletExchangeMigrationTest(t: GlobalTestState) { await exchange2.start(); + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchange.baseUrl, + force: true, + }); + try { - await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { exchangeBaseUrl: exchange.baseUrl, - force: true, + forceUpdate: true, }); } catch (e) {} diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts @@ -152,10 +152,23 @@ export async function runWalletExchangeUpdateTest( console.log("updating exchange entry"); + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchangeOne.baseUrl, + force: true, + }); + + console.log("waiting for exchange to be ready"); + + // Since the second exchange has the same base URL but + // a different public key, we expect the exchange + // entry to end up in an error state. + // Note that this might change in the future + // when we handle the case more gracefully. + await t.assertThrowsAsync(async () => { - await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { exchangeBaseUrl: exchangeOne.baseUrl, - force: true, + forceUpdate: true, }); }); diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -3823,6 +3823,27 @@ export interface TestingWaitExchangeStateRequest { walletKycStatus?: ExchangeWalletKycStatus; } +export interface TestingWaitExchangeReadyRequest { + exchangeBaseUrl: string; + /** + * Do not stop waiting even when the exchange is + * in an error state. + */ + noBail?: boolean; + /** + * Force waiting until an update really happened. + */ + forceUpdate?: boolean; +} + +export const codecForTestingWaitExchangeReadyRequest = + (): Codec<TestingWaitExchangeReadyRequest> => + buildCodecForObject<TestingWaitExchangeReadyRequest>() + .property("exchangeBaseUrl", codecForString()) + .property("noBail", codecOptional(codecForBoolean())) + .property("forceUpdate", codecOptional(codecForBoolean())) + .build("TestingWaitExchangeReadyRequest"); + export interface TransactionStatePattern { major: TransactionMajorState | TransactionStateWildcard; minor?: TransactionMinorState | TransactionStateWildcard; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -792,7 +792,8 @@ export interface ExchangeEntryRecord { tosAcceptedTimestamp: DbPreciseTimestamp | undefined; /** - * Last time when the exchange /keys info was updated. + * Last time when the exchange /keys info was updated + * successfully. */ lastUpdate: DbPreciseTimestamp | undefined; @@ -801,16 +802,6 @@ export interface ExchangeEntryRecord { */ nextUpdateStamp: DbPreciseTimestamp; - /** - * The number of times we tried to contact the exchange, - * the exchange returned a result, but it is conflicting with the - * existing exchange entry. - * - * We keep the retry counter here instead of using the task retries, - * as the shepherd task succeeded, the exchange is just not usable. - */ - updateRetryCounter?: number; - lastKeysEtag: string | undefined; /** diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -127,7 +127,6 @@ import { TransactionContext, cancelableFetch, cancelableLongPoll, - computeDbBackoff, constructTaskIdentifier, genericWaitForState, getAutoRefreshExecuteThreshold, @@ -1073,6 +1072,15 @@ async function checkExchangeEntryOutdated( return numOkay === 0; } +export interface StartUpdateExchangeResult { + /** + * Canonical or updated base URL. + */ + exchangeBaseUrl: string; + + readySummary?: ReadyExchangeSummary; +} + /** * Transition an exchange into an updating state. * @@ -1091,7 +1099,7 @@ export async function startUpdateExchangeEntry( wex: WalletExecutionContext, exchangeBaseUrl: string, options: { forceUpdate?: boolean; forceUnavailable?: boolean } = {}, -): Promise<void> { +): Promise<StartUpdateExchangeResult> { logger.trace( `starting update of exchange entry ${exchangeBaseUrl}, forced=${ options.forceUpdate ?? false @@ -1099,21 +1107,44 @@ export async function startUpdateExchangeEntry( ); await wex.runLegacyWalletDbTx(async (tx) => { + const rec = await tx.exchangeBaseUrlFixups.get(exchangeBaseUrl); + if (rec) { + logger.warn( + `using replacement ${rec.replacement} for ${exchangeBaseUrl}`, + ); + exchangeBaseUrl = rec.replacement; + } + }); + + await wex.runLegacyWalletDbTx(async (tx) => { wex.ws.exchangeCache.clear(); return provideExchangeRecordInTx(wex.ws, tx, exchangeBaseUrl); }); + if (!options.forceUpdate) { + const cachedResp = wex.ws.exchangeCache.get(exchangeBaseUrl); + if (cachedResp) { + return cachedResp; + } + } else { + wex.ws.exchangeCache.clear(); + } + + let readySummary: ReadyExchangeSummary | undefined = undefined; + const res = await wex.runLegacyWalletDbTx(async (tx) => { const r = await tx.exchanges.get(exchangeBaseUrl); if (!r) { throw Error("exchange not found"); } + const taskId = TaskIdentifiers.forExchangeUpdate(r); + const oldExchangeState = getExchangeState(r); if (options.forceUnavailable) { switch (r.updateStatus) { case ExchangeEntryDbUpdateStatus.UnavailableUpdate: - return undefined; + break; default: r.lastUpdate = undefined; r.nextUpdateStamp = timestampPreciseToDb(TalerPreciseTimestamp.now()); @@ -1138,6 +1169,7 @@ export async function startUpdateExchangeEntry( r.updateStatus = ExchangeEntryDbUpdateStatus.OutdatedUpdate; } else { r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate; + readySummary = await loadReadyExchangeSummary(tx, r); } r.cachebreakNextUpdate = options.forceUpdate; break; @@ -1155,6 +1187,7 @@ export async function startUpdateExchangeEntry( options.forceUpdate || AbsoluteTime.isExpired(nextUpdateTimestamp) ) ) { + readySummary = await loadReadyExchangeSummary(tx, r); return undefined; } const outdated = await checkExchangeEntryOutdated( @@ -1182,7 +1215,6 @@ export async function startUpdateExchangeEntry( wex.ws.exchangeCache.clear(); await tx.exchanges.put(r); const newExchangeState = getExchangeState(r); - const taskId = TaskIdentifiers.forExchangeUpdate(r); tx.notify({ type: NotificationType.ExchangeStateTransition, exchangeBaseUrl, @@ -1191,20 +1223,21 @@ export async function startUpdateExchangeEntry( }); return { taskId }; }); - if (!res) { - // Exchange entry is already good. - logger.trace(`exchange entry already up to date`); - return; - } - const { taskId } = res; - logger.info(`updating exchange in task ${taskId}`); - - if (options.forceUpdate) { - await wex.taskScheduler.resetTask(taskId); - } else { - wex.taskScheduler.startShepherdTask(taskId); + if (res) { + const { taskId } = res; + if (options.forceUpdate) { + // FIXME: Do we throttle forced updates somehow? + await wex.taskScheduler.resetTask(taskId); + } else { + wex.taskScheduler.startShepherdTask(taskId); + } } + + return { + exchangeBaseUrl: exchangeBaseUrl, + readySummary, + }; } /** @@ -1298,34 +1331,16 @@ export async function fetchFreshExchange( ): Promise<ReadyExchangeSummary> { logger.trace(`fetch fresh ${baseUrl} forced ${options.forceUpdate}`); - if (!options.forceUpdate) { - const cachedResp = wex.ws.exchangeCache.get(baseUrl); - if (cachedResp) { - return cachedResp; - } - } else { - wex.ws.exchangeCache.clear(); - } - - await wex.runLegacyWalletDbTx(async (tx) => { - const rec = await tx.exchangeBaseUrlFixups.get(baseUrl); - if (rec) { - logger.warn(`using replacement ${rec.replacement} for ${baseUrl}`); - baseUrl = rec.replacement; - } - }); - - // FIXME: We should only transition here when - // the update is forced or necessary! - - await wex.taskScheduler.ensureRunning(); - - await startUpdateExchangeEntry(wex, baseUrl, { + const startRes = await startUpdateExchangeEntry(wex, baseUrl, { forceUpdate: options.forceUpdate, }); - const resp = await waitReadyExchange(wex, baseUrl, options); - return resp; + if (startRes.readySummary) { + // Fast path: Ready exchange is already available! + return startRes.readySummary; + } + + return await waitReadyExchange(wex, startRes.exchangeBaseUrl, options); } /** @@ -1338,7 +1353,7 @@ export async function fetchFreshExchange( * the exchange is really not updating anymore, * even when the exchange entry still looks recent enough. */ -async function waitReadyExchange( +export async function waitReadyExchange( wex: WalletExecutionContext, exchangeBaseUrl: string, options: { @@ -1349,6 +1364,16 @@ async function waitReadyExchange( ): Promise<ReadyExchangeSummary> { logger.trace(`waiting for exchange ${exchangeBaseUrl} to become ready`); + await wex.runLegacyWalletDbTx(async (tx) => { + const rec = await tx.exchangeBaseUrlFixups.get(exchangeBaseUrl); + if (rec) { + logger.warn( + `using replacement ${rec.replacement} for ${exchangeBaseUrl}`, + ); + exchangeBaseUrl = rec.replacement; + } + }); + const operationId = constructTaskIdentifier({ tag: PendingTaskType.ExchangeUpdate, exchangeBaseUrl: exchangeBaseUrl, @@ -1380,7 +1405,9 @@ async function waitReadyExchange( }); if (!exchange) { - throw Error("exchange entry does not exist anymore"); + throw Error( + `exchange entry for ${exchangeBaseUrl} does not exist anymore`, + ); } let ready = false; @@ -1462,23 +1489,11 @@ async function waitReadyExchange( throw Error("invariant failed"); } - const mySummary: ReadyExchangeSummary = { - currency: exchangeDetails.currency, - exchangeBaseUrl: exchangeBaseUrl, - masterPub: exchangeDetails.masterPublicKey, - tosStatus: getExchangeTosStatusFromRecord(exchange), - tosAcceptedEtag: exchange.tosAcceptedEtag, - wireInfo: exchangeDetails.wireInfo, - protocolVersionRange: exchangeDetails.protocolVersionRange, - tosCurrentEtag: exchange.tosCurrentEtag, - tosAcceptedTimestamp: timestampOptionalPreciseFromDb( - exchange.tosAcceptedTimestamp, - ), + const mySummary = constructReadyExchangeSummary( + exchange, + exchangeDetails, scopeInfo, - walletBalanceLimitWithoutKyc: exchangeDetails.walletBalanceLimits, - hardLimits: exchangeDetails.hardLimits ?? [], - zeroLimits: exchangeDetails.zeroLimits ?? [], - }; + ); if (options.expectedMasterPub) { if (mySummary.masterPub !== options.expectedMasterPub) { @@ -1497,6 +1512,48 @@ async function waitReadyExchange( return res; } +function constructReadyExchangeSummary( + exchangeRec: ExchangeEntryRecord, + exchangeDetails: ExchangeDetailsRecord, + scopeInfo: ScopeInfo, +): ReadyExchangeSummary { + return { + currency: exchangeDetails.currency, + exchangeBaseUrl: exchangeRec.baseUrl, + masterPub: exchangeDetails.masterPublicKey, + tosStatus: getExchangeTosStatusFromRecord(exchangeRec), + tosAcceptedEtag: exchangeRec.tosAcceptedEtag, + wireInfo: exchangeDetails.wireInfo, + protocolVersionRange: exchangeDetails.protocolVersionRange, + tosCurrentEtag: exchangeRec.tosCurrentEtag, + tosAcceptedTimestamp: timestampOptionalPreciseFromDb( + exchangeRec.tosAcceptedTimestamp, + ), + scopeInfo, + walletBalanceLimitWithoutKyc: exchangeDetails.walletBalanceLimits, + hardLimits: exchangeDetails.hardLimits ?? [], + zeroLimits: exchangeDetails.zeroLimits ?? [], + }; +} + +async function loadReadyExchangeSummary( + tx: LegacyWalletTxHandle, + exchangeRec: ExchangeEntryRecord, +): Promise<ReadyExchangeSummary | undefined> { + const exchangeDetails = await getExchangeRecordsInternal( + tx, + exchangeRec.baseUrl, + ); + if (!exchangeDetails) { + return undefined; + } + const scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails); + if (!scopeInfo) { + return undefined; + } + return constructReadyExchangeSummary(exchangeRec, exchangeDetails, scopeInfo); +} + function checkPeerPaymentsDisabled(keysInfo: ExchangeKeysResponse): boolean { const now = AbsoluteTime.now(); for (let gf of keysInfo.global_fees) { @@ -1555,11 +1612,11 @@ async function handleExchageUpdateIncompatible( exchangeBaseUrl: string, exchangeProtocolVersion: string, ): Promise<TaskRunResult> { - const updated = await wex.runLegacyWalletDbTx(async (tx) => { + await wex.runLegacyWalletDbTx(async (tx) => { const r = await tx.exchanges.get(exchangeBaseUrl); if (!r) { logger.warn(`exchange ${exchangeBaseUrl} no longer present`); - return undefined; + return; } switch (r.updateStatus) { case ExchangeEntryDbUpdateStatus.InitialUpdate: @@ -1568,11 +1625,9 @@ async function handleExchageUpdateIncompatible( case ExchangeEntryDbUpdateStatus.UnavailableUpdate: break; default: - return undefined; + return; } const oldExchangeState = getExchangeState(r); - r.updateRetryCounter = (r.updateRetryCounter ?? 0) + 1; - r.nextUpdateStamp = computeDbBackoff(r.updateRetryCounter); r.updateStatus = ExchangeEntryDbUpdateStatus.UnavailableUpdate; r.unavailableReason = makeTalerErrorDetail( TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, @@ -1583,22 +1638,14 @@ async function handleExchageUpdateIncompatible( ); const newExchangeState = getExchangeState(r); await tx.exchanges.put(r); - return { - oldExchangeState, - newExchangeState, - }; - }); - if (updated) { - wex.ws.notify({ + tx.notify({ type: NotificationType.ExchangeStateTransition, exchangeBaseUrl, - newExchangeState: updated.newExchangeState, - oldExchangeState: updated.oldExchangeState, + newExchangeState, + oldExchangeState, }); - } - // Next invocation will cause the task to be run again - // at the necessary time. - return TaskRunResult.progress(); + }); + return TaskRunResult.backoff(); } /** @@ -1872,11 +1919,11 @@ export async function updateExchangeFromUrlHandler( } } - const updated = await wex.runLegacyWalletDbTx(async (tx) => { + const taskRes = await wex.runLegacyWalletDbTx(async (tx) => { const r = await tx.exchanges.get(exchangeBaseUrl); if (!r) { logger.warn(`exchange ${exchangeBaseUrl} no longer present`); - return; + return TaskRunResult.progress(); } wex.ws.clearAllCaches(); @@ -1914,20 +1961,20 @@ export async function updateExchangeFromUrlHandler( detail: conflictHint, }, ); - r.updateRetryCounter = (r.updateRetryCounter ?? 0) + 1; - r.nextUpdateStamp = computeDbBackoff(r.updateRetryCounter); r.nextRefreshCheckStamp = timestampPreciseToDb( AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()), ); r.cachebreakNextUpdate = true; await tx.exchanges.put(r); - return { - oldExchangeState, + tx.notify({ + type: NotificationType.ExchangeStateTransition, + exchangeBaseUrl, newExchangeState: getExchangeState(r), - }; + oldExchangeState, + }); + return TaskRunResult.backoff(); } delete r.unavailableReason; - r.updateRetryCounter = 0; const newDetails: ExchangeDetailsRecord = { auditors: keysInfo.auditors, currency: keysInfo.currency, @@ -2148,12 +2195,7 @@ export async function updateExchangeFromUrlHandler( logger.trace("done updating denominations in database"); - const denomLossResult = await handleDenomLoss( - wex, - tx, - newDetails.currency, - exchangeBaseUrl, - ); + await handleDenomLoss(wex, tx, newDetails.currency, exchangeBaseUrl); if (keysInfo.recoup != null) { await handleRecoup(wex, tx, exchangeBaseUrl, keysInfo.recoup); @@ -2161,34 +2203,18 @@ export async function updateExchangeFromUrlHandler( const newExchangeState = getExchangeState(r); - return { - exchange: r, - exchangeDetails: newDetails, - oldExchangeState, + tx.notify({ + type: NotificationType.ExchangeStateTransition, + exchangeBaseUrl, newExchangeState, - denomLossResult, - }; - }); - - if (!updated) { - throw Error("something went wrong with updating the exchange"); - } + oldExchangeState, + }); - if (updated.denomLossResult) { - for (const notif of updated.denomLossResult.notifications) { - wex.ws.notify(notif); - } - } + return TaskRunResult.progress(); + }); logger.trace("done updating exchange info in database"); - wex.ws.notify({ - type: NotificationType.ExchangeStateTransition, - exchangeBaseUrl, - newExchangeState: updated.newExchangeState, - oldExchangeState: updated.oldExchangeState, - }); - // Always trigger auto-refresh after an exchange update. await doExchangeAutoRefresh(wex, exchangeBaseUrl); @@ -2200,9 +2226,7 @@ export async function updateExchangeFromUrlHandler( constructTaskIdentifier({ tag: PendingTaskType.ValidateDenoms }), ); - // Next invocation will cause the task to be run again - // at the necessary time. - return TaskRunResult.progress(); + return taskRes; } async function doExchangeAutoRefresh( @@ -2321,16 +2345,12 @@ export async function processTaskExchangeAutoRefresh( return TaskRunResult.progress(); } -interface DenomLossResult { - notifications: WalletNotification[]; -} - async function handleDenomLoss( wex: WalletExecutionContext, tx: WalletIndexedDbTransaction, currency: string, exchangeBaseUrl: string, -): Promise<DenomLossResult> { +): Promise<void> { const coinAvailabilityRecs = await tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl); const denomsVanished: string[] = []; @@ -2340,10 +2360,6 @@ async function handleDenomLoss( let amountExpired = Amount.zeroOfCurrency(currency); let amountUnoffered = Amount.zeroOfCurrency(currency); - const result: DenomLossResult = { - notifications: [], - }; - for (const coinAv of coinAvailabilityRecs) { if (coinAv.freshCoinCount <= 0) { continue; @@ -2420,7 +2436,7 @@ async function handleDenomLoss( await tx.denomLossEvents.add(rec); const ctx = new DenomLossTransactionContext(wex, denomLossEventId); await ctx.updateTransactionMeta(tx); - result.notifications.push({ + tx.notify({ type: NotificationType.TransactionStateTransition, transactionId: ctx.transactionId, oldTxState: { @@ -2431,7 +2447,7 @@ async function handleDenomLoss( }, newStId: rec.status, }); - result.notifications.push({ + tx.notify({ type: NotificationType.BalanceChange, hintTransactionId: ctx.transactionId, }); @@ -2452,7 +2468,7 @@ async function handleDenomLoss( await tx.denomLossEvents.add(rec); const ctx = new DenomLossTransactionContext(wex, denomLossEventId); await ctx.updateTransactionMeta(tx); - result.notifications.push({ + tx.notify({ type: NotificationType.TransactionStateTransition, transactionId: ctx.transactionId, oldTxState: { @@ -2463,7 +2479,7 @@ async function handleDenomLoss( }, newStId: rec.status, }); - result.notifications.push({ + tx.notify({ type: NotificationType.BalanceChange, hintTransactionId: ctx.transactionId, }); @@ -2486,7 +2502,7 @@ async function handleDenomLoss( tag: TransactionType.DenomLoss, denomLossEventId, }); - result.notifications.push({ + tx.notify({ type: NotificationType.TransactionStateTransition, transactionId, oldTxState: { @@ -2497,13 +2513,11 @@ async function handleDenomLoss( }, newStId: rec.status, }); - result.notifications.push({ + tx.notify({ type: NotificationType.BalanceChange, hintTransactionId: transactionId, }); } - - return result; } export function computeDenomLossTransactionStatus( diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -183,6 +183,7 @@ import { TestingGetReserveHistoryRequest, TestingPlanMigrateExchangeBaseUrlRequest, TestingSetTimetravelRequest, + TestingWaitExchangeReadyRequest, TestingWaitExchangeStateRequest, TestingWaitTransactionRequest, TestingWaitWalletKycRequest, @@ -363,6 +364,7 @@ export enum WalletApiOperation { TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", TestingWaitTransactionState = "testingWaitTransactionState", TestingWaitExchangeState = "testingWaitExchangeState", + TestingWaitExchangeReady = "testingWaitExchangeReady", TestingWaitTasksDone = "testingWaitTasksDone", TestingGetDbStats = "testingGetDbStats", TestingSetTimetravel = "testingSetTimetravel", @@ -991,6 +993,13 @@ export type AddExchangeOp = { /** * Update an exchange entry. + * + * Only starts updating the exchange entry. + * After this request finishes, it is not guaranteed that + * the exchange entry has been updated. + * + * Use notifications and the listExchanges request + * to check the status. */ export type UpdateExchangeEntryOp = { op: WalletApiOperation.UpdateExchangeEntry; @@ -1505,11 +1514,22 @@ export type TestingWaitTransactionStateOp = { * Wait until an exchange entry is in a particular state. */ export type TestingWaitExchangeStateOp = { - op: WalletApiOperation.TestingWaitTransactionState; + op: WalletApiOperation.TestingWaitExchangeState; request: TestingWaitExchangeStateRequest; response: EmptyObject; }; +/** + * Wait until an exchange entry is ready. + * Returns an error if updating the exchange + * failed. + */ +export type TestingWaitExchangeReadyOp = { + op: WalletApiOperation.TestingWaitExchangeReady; + request: TestingWaitExchangeReadyRequest; + response: EmptyObject; +}; + export type TestingPingOp = { op: WalletApiOperation.TestingPing; request: EmptyObject; @@ -1663,6 +1683,7 @@ export type WalletOperations = { [WalletApiOperation.TestingGetDbStats]: TestingGetDbStats; [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp; [WalletApiOperation.TestingWaitExchangeState]: TestingWaitExchangeStateOp; + [WalletApiOperation.TestingWaitExchangeReady]: TestingWaitExchangeReadyOp; [WalletApiOperation.TestingWaitTasksDone]: TestingWaitTasksDoneOp; [WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp; [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -150,6 +150,7 @@ import { TestingGetFlightRecordsResponse, TestingGetReserveHistoryRequest, TestingSetTimetravelRequest, + TestingWaitExchangeReadyRequest, TimerAPI, TimerGroup, TransactionType, @@ -261,6 +262,7 @@ import { codecForTestingGetReserveHistoryRequest, codecForTestingPlanMigrateExchangeBaseUrlRequest, codecForTestingSetTimetravelRequest, + codecForTestingWaitExchangeReadyRequest, codecForTestingWaitWalletKycRequest, codecForTransactionByIdRequest, codecForTransactionsRequest, @@ -349,6 +351,8 @@ import { listExchanges, lookupExchangeByUri, markExchangeUsed, + startUpdateExchangeEntry, + waitReadyExchange, } from "./exchanges.js"; import { convertDepositAmount } from "./instructedAmountConversion.js"; import { @@ -1062,6 +1066,9 @@ async function handleAddExchange( ); } + // FIXME: Check /config before adding the exchange entry. + + // FIXME: We probably should not wait synchronously here. await fetchFreshExchange(wex, exchangeBaseUrl, {}); // Exchange has been explicitly added upon user request. // Thus, we mark it as "used". @@ -1079,8 +1086,9 @@ async function handleUpdateExchangeEntry( wex: WalletExecutionContext, req: UpdateExchangeEntryRequest, ): Promise<EmptyObject> { - await fetchFreshExchange(wex, req.exchangeBaseUrl, { + await startUpdateExchangeEntry(wex, req.exchangeBaseUrl, { forceUpdate: !!req.force, + forceUnavailable: !!req.force, }); return {}; } @@ -2067,6 +2075,17 @@ export async function handleGetDiagnostics( }; } +export async function handleTestingWaitExchangeReady( + wex: WalletExecutionContext, + req: TestingWaitExchangeReadyRequest, +): Promise<EmptyObject> { + await waitReadyExchange(wex, req.exchangeBaseUrl, { + noBail: req.noBail, + forceUpdate: req.forceUpdate, + }); + return {}; +} + export async function handleGetPerformanceStats( wex: WalletExecutionContext, req: GetPerformanceStatsRequest, @@ -2085,6 +2104,10 @@ interface HandlerWithValidator<Tag extends WalletApiOperation> { } const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { + [WalletApiOperation.TestingWaitExchangeReady]: { + codec: codecForTestingWaitExchangeReadyRequest(), + handler: handleTestingWaitExchangeReady, + }, [WalletApiOperation.TestingCorruptWithdrawalCoinSel]: { codec: codecForTestingCorruptWithdrawalCoinSelRequest(), handler: handleTestingCorruptWithdrawalCoinSel,