taler-typescript-core

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

commit bc269e0ff64f00892a8a1947c4e468db3d5aff13
parent 93ea97532b11cfd98eb9bc841c59478b057675ca
Author: Florian Dold <florian@dold.me>
Date:   Mon, 25 Aug 2025 01:08:24 +0200

wallet-core: make base url migration work with existing new exchange, other tweaks

Diffstat:
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 8+++++---
Mpackages/taler-wallet-core/src/db.ts | 14+++++++++++++-
Mpackages/taler-wallet-core/src/exchanges.ts | 44+++++++++++++++++++++++++++++++++++++-------
Mpackages/taler-wallet-core/src/withdraw.ts | 16+++++++++++++++-
4 files changed, 70 insertions(+), 12 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -100,6 +100,9 @@ import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js" import { runMerchantInstancesTest } from "./test-merchant-instances.js"; import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js"; import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; +import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js"; +import { runMerchantSelfProvisionForgotPasswordTest } from "./test-merchant-self-provision-forgot-password.js"; +import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js"; import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; import { runMultiExchangeTest } from "./test-multiexchange.js"; import { runOtpTest } from "./test-otp.js"; @@ -161,6 +164,7 @@ import { runWalletDd48Test } from "./test-wallet-dd48.js"; import { runWalletDenomExpireTest } from "./test-wallet-denom-expire.js"; import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js"; import { runWalletDevexpFakeprotoverTest } from "./test-wallet-devexp-fakeprotover.js"; +import { runWalletExchangeMigrationExistingTest } from "./test-wallet-exchange-migration-existing.js"; import { runWalletExchangeMigrationTest } from "./test-wallet-exchange-migration.js"; import { runWalletExchangeUpdateTest } from "./test-wallet-exchange-update.js"; import { runWalletGenDbTest } from "./test-wallet-gendb.js"; @@ -189,9 +193,6 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; -import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js"; -import { runMerchantSelfProvisionForgotPasswordTest } from "./test-merchant-self-provision-forgot-password.js"; -import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js"; /** * Test runner. @@ -366,6 +367,7 @@ const allTests: TestMainFunction[] = [ runExchangeKycAuthTest, runTopsPeerTest, runWalletExchangeMigrationTest, + runWalletExchangeMigrationExistingTest, runKycFormCompressionTest, runDepositLargeTest, runDepositTooLargeTest, diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -163,7 +163,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 18; +export const WALLET_DB_MINOR_VERSION = 19; declare const symDbProtocolTimestamp: unique symbol; @@ -2791,6 +2791,11 @@ export interface ExchangeMigrationLogRecord { reason: ExchangeMigrationReason; } +export interface ExchangeBaseUrlFixupRecord { + exchangeBaseUrl: string; + replacement: string; +} + /** * Schema definition for the IndexedDB * wallet database. @@ -2803,6 +2808,13 @@ export const WalletStoresV1 = { versionAdded: 18, indexes: {}, }), + exchangeBaseUrlFixups: describeStoreV2({ + recordCodec: passthroughCodec<ExchangeBaseUrlFixupRecord>(), + storeName: "exchangeBaseUrlFixups", + keyPath: "exchangeBaseUrl", + versionAdded: 19, + indexes: {}, + }), denomLossEvents: describeStoreV2({ recordCodec: passthroughCodec<DenomLossEventRecord>(), storeName: "denomLossEvents", diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -45,6 +45,7 @@ import { EmptyObject, ExchangeAuditor, ExchangeDetailedResponse, + ExchangeEntryState, ExchangeGlobalFees, ExchangeListItem, ExchangeSignKeyJson, @@ -1388,6 +1389,17 @@ export async function fetchFreshExchange( wex.ws.exchangeCache.clear(); } + await wex.db.runReadOnlyTx( + { storeNames: ["exchangeBaseUrlFixups"] }, + async (tx) => { + const rec = await tx.exchangeBaseUrlFixups.get(baseUrl); + if (rec) { + logger.warn(`using replacement ${rec.replacement} for ${baseUrl}`); + baseUrl = rec.replacement; + } + }, + ); + await wex.taskScheduler.ensureRunning(); await startUpdateExchangeEntry(wex, baseUrl, { @@ -4134,6 +4146,8 @@ export async function migrateExchange( return; } + let existingNewExchangeSt: ExchangeEntryState | undefined = undefined; + await tx.exchangeBaseUrlMigrationLog.put({ oldExchangeBaseUrl: req.oldExchangeBaseUrl, newExchangeBaseUrl: req.newExchangeBaseUrl, @@ -4141,6 +4155,11 @@ export async function migrateExchange( reason: req.trigger, }); + await tx.exchangeBaseUrlFixups.put({ + exchangeBaseUrl: req.oldExchangeBaseUrl, + replacement: req.newExchangeBaseUrl, + }); + { const denomKeys = await tx.denominations.indexes.byExchangeBaseUrl.getAllKeys( @@ -4176,17 +4195,28 @@ export async function migrateExchange( } { - await tx.exchangeDetails.indexes.byExchangeBaseUrl - .iter(req.oldExchangeBaseUrl) - .forEachAsync(async (rec) => { - rec.exchangeBaseUrl = req.newExchangeBaseUrl; - await tx.exchangeDetails.put(rec); - }); + const existingNewExchangeDetails = + await tx.exchangeDetails.indexes.byExchangeBaseUrl.get( + req.newExchangeBaseUrl, + ); + if (!existingNewExchangeDetails) { + await tx.exchangeDetails.indexes.byExchangeBaseUrl + .iter(req.oldExchangeBaseUrl) + .forEachAsync(async (rec) => { + rec.exchangeBaseUrl = req.newExchangeBaseUrl; + await tx.exchangeDetails.put(rec); + }); + } } { const rec = await tx.exchanges.get(req.oldExchangeBaseUrl); if (rec) { + existingNewExchangeSt = { + exchangeEntryStatus: getExchangeEntryStatusFromRecord(rec), + exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(rec), + tosStatus: getExchangeTosStatusFromRecord(rec), + }; rec.baseUrl = req.newExchangeBaseUrl; await tx.exchanges.delete(req.oldExchangeBaseUrl); await tx.exchanges.put(rec); @@ -4299,7 +4329,7 @@ export async function migrateExchange( tx.notify({ type: NotificationType.ExchangeStateTransition, exchangeBaseUrl: req.newExchangeBaseUrl, - oldExchangeState: undefined, + oldExchangeState: existingNewExchangeSt, newExchangeState: getExchangeState(exch), }); }); diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -1187,6 +1187,8 @@ async function processWithdrawalGroupDialogProposed( switch (resp.status) { case HttpStatusCode.NotFound: { // FIXME: Further inspect the error body + const err = await readTalerErrorResponse(resp); + logger.warn(`withdrawal operation not found, aborting: ${j2s(err)}`); await transitionSimple( ctx, WithdrawalGroupStatus.DialogProposed, @@ -3148,6 +3150,8 @@ async function registerReserveWithBank( switch (httpResp.status) { case HttpStatusCode.NotFound: { // FIXME: Inspect particular status code + const err = await readTalerErrorResponse(httpResp); + logger.warn(`withdrawal operation not found, aborting: ${j2s(err)}`); await transitionSimple( ctx, WithdrawalGroupStatus.PendingRegisteringBank, @@ -3800,7 +3804,7 @@ export async function confirmWithdrawal( req: ConfirmWithdrawalRequest, ): Promise<AcceptWithdrawalResponse> { const parsedTx = parseTransactionIdentifier(req.transactionId); - const selectedExchange = req.exchangeBaseUrl; + let selectedExchange: string = req.exchangeBaseUrl; const instructedAmount = req.amount == null ? undefined : Amounts.parseOrThrow(req.amount); @@ -3823,6 +3827,16 @@ export async function confirmWithdrawal( throw Error("not a bank integrated withdrawal"); } + await wex.db.runReadOnlyTx( + { storeNames: ["exchangeBaseUrlFixups"] }, + async (tx) => { + const rec = await tx.exchangeBaseUrlFixups.get(selectedExchange); + if (rec) { + selectedExchange = rec.replacement; + } + }, + ); + let instructedCurrency: string; if (instructedAmount) { instructedCurrency = instructedAmount.currency;