diff options
8 files changed, 84 insertions, 22 deletions
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 9d95f1ee2..5ff906faa 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -904,6 +904,10 @@ export enum ExchangeEntryStatus { Ok = "ok", } +export interface OperationErrorInfo { + error: TalerErrorDetail; +} + // FIXME: This should probably include some error status. export interface ExchangeListItem { exchangeBaseUrl: string; @@ -917,6 +921,12 @@ export interface ExchangeListItem { * temporarily queried. */ permanent: boolean; + + /** + * Information about the last error that occured when trying + * to update the exchange info. + */ + lastUpdateErrorInfo?: OperationErrorInfo; } const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> => diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts index f0b4ac8c9..6b63c3741 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts @@ -194,12 +194,16 @@ export async function runExchangeManagementTest( t.assertTrue( err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, ); - exchangesList = await wallet.client.call( WalletApiOperation.ListExchanges, {}, ); - t.assertTrue(exchangesList.exchanges.length === 0); + console.log("exchanges list", j2s(exchangesList)); + t.assertTrue(exchangesList.exchanges.length === 1); + t.assertTrue( + exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code === + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + ); /* * ========================================================================= @@ -240,7 +244,11 @@ export async function runExchangeManagementTest( WalletApiOperation.ListExchanges, {}, ); - t.assertTrue(exchangesList.exchanges.length === 0); + t.assertTrue(exchangesList.exchanges.length === 1); + t.assertTrue( + exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code === + TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, + ); /* * ========================================================================= diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index e29065390..f35556736 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -28,6 +28,7 @@ import { ExchangeTosStatus, j2s, Logger, + OperationErrorInfo, RefreshReason, TalerErrorCode, TalerErrorDetail, @@ -224,30 +225,37 @@ export async function storeOperationPending( }); } -export async function runOperationWithErrorReporting( +export async function runOperationWithErrorReporting<T1, T2>( ws: InternalWalletState, opId: string, - f: () => Promise<OperationAttemptResult>, -): Promise<void> { + f: () => Promise<OperationAttemptResult<T1, T2>>, +): Promise<OperationAttemptResult<T1, T2>> { let maybeError: TalerErrorDetail | undefined; try { const resp = await f(); switch (resp.type) { case OperationAttemptResultType.Error: - return await storeOperationError(ws, opId, resp.errorDetail); + await storeOperationError(ws, opId, resp.errorDetail); + return resp; case OperationAttemptResultType.Finished: - return await storeOperationFinished(ws, opId); + await storeOperationFinished(ws, opId); + return resp; case OperationAttemptResultType.Pending: - return await storeOperationPending(ws, opId); + await storeOperationPending(ws, opId); + return resp; case OperationAttemptResultType.Longpoll: - break; + return resp; } } catch (e) { if (e instanceof TalerError) { logger.warn("operation processed resulted in error"); logger.warn(`error was: ${j2s(e.errorDetail)}`); maybeError = e.errorDetail; - return await storeOperationError(ws, opId, maybeError!); + await storeOperationError(ws, opId, maybeError!); + return { + type: OperationAttemptResultType.Error, + errorDetail: e.errorDetail, + }; } else if (e instanceof Error) { // This is a bug, as we expect pending operations to always // do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED @@ -261,7 +269,11 @@ export async function runOperationWithErrorReporting( }, `unexpected exception (message: ${e.message})`, ); - return await storeOperationError(ws, opId, maybeError); + await storeOperationError(ws, opId, maybeError); + return { + type: OperationAttemptResultType.Error, + errorDetail: maybeError, + }; } else { logger.error("Uncaught exception, value is not even an error."); maybeError = makeErrorDetail( @@ -269,7 +281,11 @@ export async function runOperationWithErrorReporting( {}, `unexpected exception (not even an error)`, ); - return await storeOperationError(ws, opId, maybeError); + await storeOperationError(ws, opId, maybeError); + return { + type: OperationAttemptResultType.Error, + errorDetail: maybeError, + }; } } } @@ -357,7 +373,13 @@ export function getExchangeTosStatus( export function makeExchangeListItem( r: ExchangeRecord, exchangeDetails: ExchangeDetailsRecord | undefined, + lastError: TalerErrorDetail | undefined, ): ExchangeListItem { + const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError + ? { + error: lastError, + } + : undefined; if (!exchangeDetails) { return { exchangeBaseUrl: r.baseUrl, @@ -367,6 +389,7 @@ export function makeExchangeListItem( exchangeStatus: ExchangeEntryStatus.Unknown, permanent: r.permanent, ageRestrictionOptions: [], + lastUpdateErrorInfo, }; } let exchangeStatus; @@ -381,5 +404,6 @@ export function makeExchangeListItem( ageRestrictionOptions: exchangeDetails.ageMask ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask) : [], + lastUpdateErrorInfo, }; } diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 41e63b956..23ff1479e 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -73,9 +73,11 @@ import { import { OperationAttemptResult, OperationAttemptResultType, - runOperationHandlerForResult, + RetryTags, + unwrapOperationHandlerResultOrThrow, } from "../util/retries.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js"; +import { runOperationWithErrorReporting } from "./common.js"; import { isWithdrawableDenom } from "./withdraw.js"; const logger = new Logger("exchanges.ts"); @@ -546,8 +548,13 @@ export async function updateExchangeFromUrl( exchange: ExchangeRecord; exchangeDetails: ExchangeDetailsRecord; }> { - return runOperationHandlerForResult( - await updateExchangeFromUrlHandler(ws, baseUrl, options), + const canonUrl = canonicalizeBaseUrl(baseUrl); + return unwrapOperationHandlerResultOrThrow( + await runOperationWithErrorReporting( + ws, + RetryTags.forExchangeUpdateFromUrl(canonUrl), + () => updateExchangeFromUrlHandler(ws, canonUrl, options), + ), ); } diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index c2df6115b..e92c805bd 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -54,7 +54,7 @@ import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; import { OperationAttemptResult, - runOperationHandlerForResult, + unwrapOperationHandlerResultOrThrow, } from "../util/retries.js"; import { createRefreshGroup, processRefreshGroup } from "./refresh.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js"; @@ -307,7 +307,7 @@ export async function processRecoupGroup( forceNow?: boolean; } = {}, ): Promise<void> { - await runOperationHandlerForResult( + await unwrapOperationHandlerResultOrThrow( await processRecoupGroupHandler(ws, recoupGroupId, options), ); return; diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 0e6bb8339..a9ecdf369 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -1385,6 +1385,7 @@ export async function getWithdrawalDetailsForUri( x.exchangeDetails, x.exchangeTos, x.denominations, + x.operationRetries, ]) .runReadOnly(async (tx) => { const exchangeRecords = await tx.exchanges.iter().toArray(); @@ -1396,8 +1397,13 @@ export async function getWithdrawalDetailsForUri( const denominations = await tx.denominations.indexes.byExchangeBaseUrl .iter(r.baseUrl) .toArray(); + const retryRecord = await tx.operationRetries.get( + RetryTags.forExchangeUpdate(r), + ); if (exchangeDetails && denominations) { - exchanges.push(makeExchangeListItem(r, exchangeDetails)); + exchanges.push( + makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError), + ); } } }); diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 5e1089dc5..8861d4d1e 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -176,6 +176,9 @@ export namespace RetryTags { export function forExchangeUpdate(exch: ExchangeRecord): string { return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`; } + export function forExchangeUpdateFromUrl(exchBaseUrl: string): string { + return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}`; + } export function forExchangeCheckRefresh(exch: ExchangeRecord): string { return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`; } @@ -246,7 +249,7 @@ export async function scheduleRetry( /** * Run an operation handler, expect a success result and extract the success value. */ -export async function runOperationHandlerForResult<T>( +export async function unwrapOperationHandlerResultOrThrow<T>( res: OperationAttemptResult<T>, ): Promise<T> { switch (res.type) { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index dbab6607e..d7d8a206f 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -239,7 +239,7 @@ import { GetReadOnlyAccess, GetReadWriteAccess, } from "./util/query.js"; -import { OperationAttemptResult } from "./util/retries.js"; +import { OperationAttemptResult, RetryTags } from "./util/retries.js"; import { TimerAPI, TimerGroup } from "./util/timer.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, @@ -650,12 +650,16 @@ async function getExchanges( x.exchangeDetails, x.exchangeTos, 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); - exchanges.push(makeExchangeListItem(r, exchangeDetails)); + const opRetryRecord = await tx.operationRetries.get( + RetryTags.forExchangeUpdate(r), + ); + exchanges.push(makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError)); } }); return { exchanges }; |