wallet-core: fix exchange management test case, surface exchange update error info in list
This commit is contained in:
parent
fe011321a4
commit
1e6e1a22cd
@ -904,6 +904,10 @@ export enum ExchangeEntryStatus {
|
|||||||
Ok = "ok",
|
Ok = "ok",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OperationErrorInfo {
|
||||||
|
error: TalerErrorDetail;
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: This should probably include some error status.
|
// FIXME: This should probably include some error status.
|
||||||
export interface ExchangeListItem {
|
export interface ExchangeListItem {
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
@ -917,6 +921,12 @@ export interface ExchangeListItem {
|
|||||||
* temporarily queried.
|
* temporarily queried.
|
||||||
*/
|
*/
|
||||||
permanent: boolean;
|
permanent: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the last error that occured when trying
|
||||||
|
* to update the exchange info.
|
||||||
|
*/
|
||||||
|
lastUpdateErrorInfo?: OperationErrorInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
|
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
|
||||||
|
@ -194,12 +194,16 @@ export async function runExchangeManagementTest(
|
|||||||
t.assertTrue(
|
t.assertTrue(
|
||||||
err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
||||||
exchangesList = await wallet.client.call(
|
exchangesList = await wallet.client.call(
|
||||||
WalletApiOperation.ListExchanges,
|
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,
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* =========================================================================
|
* =========================================================================
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
ExchangeTosStatus,
|
ExchangeTosStatus,
|
||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
|
OperationErrorInfo,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
@ -224,30 +225,37 @@ export async function storeOperationPending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runOperationWithErrorReporting(
|
export async function runOperationWithErrorReporting<T1, T2>(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
opId: string,
|
opId: string,
|
||||||
f: () => Promise<OperationAttemptResult>,
|
f: () => Promise<OperationAttemptResult<T1, T2>>,
|
||||||
): Promise<void> {
|
): Promise<OperationAttemptResult<T1, T2>> {
|
||||||
let maybeError: TalerErrorDetail | undefined;
|
let maybeError: TalerErrorDetail | undefined;
|
||||||
try {
|
try {
|
||||||
const resp = await f();
|
const resp = await f();
|
||||||
switch (resp.type) {
|
switch (resp.type) {
|
||||||
case OperationAttemptResultType.Error:
|
case OperationAttemptResultType.Error:
|
||||||
return await storeOperationError(ws, opId, resp.errorDetail);
|
await storeOperationError(ws, opId, resp.errorDetail);
|
||||||
|
return resp;
|
||||||
case OperationAttemptResultType.Finished:
|
case OperationAttemptResultType.Finished:
|
||||||
return await storeOperationFinished(ws, opId);
|
await storeOperationFinished(ws, opId);
|
||||||
|
return resp;
|
||||||
case OperationAttemptResultType.Pending:
|
case OperationAttemptResultType.Pending:
|
||||||
return await storeOperationPending(ws, opId);
|
await storeOperationPending(ws, opId);
|
||||||
|
return resp;
|
||||||
case OperationAttemptResultType.Longpoll:
|
case OperationAttemptResultType.Longpoll:
|
||||||
break;
|
return resp;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof TalerError) {
|
if (e instanceof TalerError) {
|
||||||
logger.warn("operation processed resulted in error");
|
logger.warn("operation processed resulted in error");
|
||||||
logger.warn(`error was: ${j2s(e.errorDetail)}`);
|
logger.warn(`error was: ${j2s(e.errorDetail)}`);
|
||||||
maybeError = 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) {
|
} else if (e instanceof Error) {
|
||||||
// This is a bug, as we expect pending operations to always
|
// This is a bug, as we expect pending operations to always
|
||||||
// do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED
|
// 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})`,
|
`unexpected exception (message: ${e.message})`,
|
||||||
);
|
);
|
||||||
return await storeOperationError(ws, opId, maybeError);
|
await storeOperationError(ws, opId, maybeError);
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Error,
|
||||||
|
errorDetail: maybeError,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
logger.error("Uncaught exception, value is not even an error.");
|
logger.error("Uncaught exception, value is not even an error.");
|
||||||
maybeError = makeErrorDetail(
|
maybeError = makeErrorDetail(
|
||||||
@ -269,7 +281,11 @@ export async function runOperationWithErrorReporting(
|
|||||||
{},
|
{},
|
||||||
`unexpected exception (not even an error)`,
|
`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(
|
export function makeExchangeListItem(
|
||||||
r: ExchangeRecord,
|
r: ExchangeRecord,
|
||||||
exchangeDetails: ExchangeDetailsRecord | undefined,
|
exchangeDetails: ExchangeDetailsRecord | undefined,
|
||||||
|
lastError: TalerErrorDetail | undefined,
|
||||||
): ExchangeListItem {
|
): ExchangeListItem {
|
||||||
|
const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
|
||||||
|
? {
|
||||||
|
error: lastError,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
return {
|
return {
|
||||||
exchangeBaseUrl: r.baseUrl,
|
exchangeBaseUrl: r.baseUrl,
|
||||||
@ -367,6 +389,7 @@ export function makeExchangeListItem(
|
|||||||
exchangeStatus: ExchangeEntryStatus.Unknown,
|
exchangeStatus: ExchangeEntryStatus.Unknown,
|
||||||
permanent: r.permanent,
|
permanent: r.permanent,
|
||||||
ageRestrictionOptions: [],
|
ageRestrictionOptions: [],
|
||||||
|
lastUpdateErrorInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let exchangeStatus;
|
let exchangeStatus;
|
||||||
@ -381,5 +404,6 @@ export function makeExchangeListItem(
|
|||||||
ageRestrictionOptions: exchangeDetails.ageMask
|
ageRestrictionOptions: exchangeDetails.ageMask
|
||||||
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
|
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
|
||||||
: [],
|
: [],
|
||||||
|
lastUpdateErrorInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -73,9 +73,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
OperationAttemptResult,
|
OperationAttemptResult,
|
||||||
OperationAttemptResultType,
|
OperationAttemptResultType,
|
||||||
runOperationHandlerForResult,
|
RetryTags,
|
||||||
|
unwrapOperationHandlerResultOrThrow,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
|
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
|
||||||
|
import { runOperationWithErrorReporting } from "./common.js";
|
||||||
import { isWithdrawableDenom } from "./withdraw.js";
|
import { isWithdrawableDenom } from "./withdraw.js";
|
||||||
|
|
||||||
const logger = new Logger("exchanges.ts");
|
const logger = new Logger("exchanges.ts");
|
||||||
@ -546,8 +548,13 @@ export async function updateExchangeFromUrl(
|
|||||||
exchange: ExchangeRecord;
|
exchange: ExchangeRecord;
|
||||||
exchangeDetails: ExchangeDetailsRecord;
|
exchangeDetails: ExchangeDetailsRecord;
|
||||||
}> {
|
}> {
|
||||||
return runOperationHandlerForResult(
|
const canonUrl = canonicalizeBaseUrl(baseUrl);
|
||||||
await updateExchangeFromUrlHandler(ws, baseUrl, options),
|
return unwrapOperationHandlerResultOrThrow(
|
||||||
|
await runOperationWithErrorReporting(
|
||||||
|
ws,
|
||||||
|
RetryTags.forExchangeUpdateFromUrl(canonUrl),
|
||||||
|
() => updateExchangeFromUrlHandler(ws, canonUrl, options),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ import { checkDbInvariant } from "../util/invariants.js";
|
|||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
import {
|
import {
|
||||||
OperationAttemptResult,
|
OperationAttemptResult,
|
||||||
runOperationHandlerForResult,
|
unwrapOperationHandlerResultOrThrow,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
|
||||||
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
||||||
@ -307,7 +307,7 @@ export async function processRecoupGroup(
|
|||||||
forceNow?: boolean;
|
forceNow?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await runOperationHandlerForResult(
|
await unwrapOperationHandlerResultOrThrow(
|
||||||
await processRecoupGroupHandler(ws, recoupGroupId, options),
|
await processRecoupGroupHandler(ws, recoupGroupId, options),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1385,6 +1385,7 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
x.exchangeDetails,
|
x.exchangeDetails,
|
||||||
x.exchangeTos,
|
x.exchangeTos,
|
||||||
x.denominations,
|
x.denominations,
|
||||||
|
x.operationRetries,
|
||||||
])
|
])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
const exchangeRecords = await tx.exchanges.iter().toArray();
|
const exchangeRecords = await tx.exchanges.iter().toArray();
|
||||||
@ -1396,8 +1397,13 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
|
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iter(r.baseUrl)
|
.iter(r.baseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
const retryRecord = await tx.operationRetries.get(
|
||||||
|
RetryTags.forExchangeUpdate(r),
|
||||||
|
);
|
||||||
if (exchangeDetails && denominations) {
|
if (exchangeDetails && denominations) {
|
||||||
exchanges.push(makeExchangeListItem(r, exchangeDetails));
|
exchanges.push(
|
||||||
|
makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -176,6 +176,9 @@ export namespace RetryTags {
|
|||||||
export function forExchangeUpdate(exch: ExchangeRecord): string {
|
export function forExchangeUpdate(exch: ExchangeRecord): string {
|
||||||
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`;
|
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`;
|
||||||
}
|
}
|
||||||
|
export function forExchangeUpdateFromUrl(exchBaseUrl: string): string {
|
||||||
|
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}`;
|
||||||
|
}
|
||||||
export function forExchangeCheckRefresh(exch: ExchangeRecord): string {
|
export function forExchangeCheckRefresh(exch: ExchangeRecord): string {
|
||||||
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`;
|
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.
|
* 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>,
|
res: OperationAttemptResult<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
switch (res.type) {
|
switch (res.type) {
|
||||||
|
@ -239,7 +239,7 @@ import {
|
|||||||
GetReadOnlyAccess,
|
GetReadOnlyAccess,
|
||||||
GetReadWriteAccess,
|
GetReadWriteAccess,
|
||||||
} from "./util/query.js";
|
} 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 { TimerAPI, TimerGroup } from "./util/timer.js";
|
||||||
import {
|
import {
|
||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
@ -650,12 +650,16 @@ async function getExchanges(
|
|||||||
x.exchangeDetails,
|
x.exchangeDetails,
|
||||||
x.exchangeTos,
|
x.exchangeTos,
|
||||||
x.denominations,
|
x.denominations,
|
||||||
|
x.operationRetries,
|
||||||
])
|
])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
const exchangeRecords = await tx.exchanges.iter().toArray();
|
const exchangeRecords = await tx.exchanges.iter().toArray();
|
||||||
for (const r of exchangeRecords) {
|
for (const r of exchangeRecords) {
|
||||||
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
|
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 };
|
return { exchanges };
|
||||||
|
Loading…
Reference in New Issue
Block a user