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:
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,