aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/exchanges.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/exchanges.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts224
1 files changed, 116 insertions, 108 deletions
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index bf7d4424a..fe6060499 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -37,6 +37,7 @@ import {
ExchangeGlobalFees,
ExchangeListItem,
ExchangeSignKeyJson,
+ ExchangeTosStatus,
ExchangeWireAccount,
ExchangesListResponse,
FeeDescription,
@@ -84,12 +85,15 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
OpenedPromise,
+ PendingTaskType,
WalletDbReadWriteTransaction,
createTimeline,
isWithdrawableDenom,
openPromise,
selectBestForOverlappingDenominations,
selectMinimumFee,
+ timestampOptionalAbsoluteFromDb,
+ timestampOptionalPreciseFromDb,
timestampPreciseFromDb,
timestampPreciseToDb,
timestampProtocolToDb,
@@ -102,9 +106,11 @@ import {
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
+ constructTaskIdentifier,
getExchangeState,
getExchangeTosStatusFromRecord,
makeExchangeListItem,
+ runTaskWithErrorReporting,
} from "./common.js";
const logger = new Logger("exchanges.ts");
@@ -629,7 +635,7 @@ async function downloadTosFromAcceptedFormat(
* If the update is forced, the exchange is put into an updating state
* even if the old information should still be up to date.
*
- * For backwards compatibility, if the exchange entry doesn't exist,
+ * If the exchange entry doesn't exist,
* a new ephemeral entry is created.
*/
export async function startUpdateExchangeEntry(
@@ -740,76 +746,18 @@ export function createNotificationWaiter(
}
/**
- * Wait until an exchange entry got successfully updated.
- *
- * Reject with an exception if the update encountered an error.
+ * Basic information about an exchange in a ready state.
*/
-export async function waitExchangeEntryUpdated(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- cancellationToken?: CancellationToken,
-): Promise<{
- exchange: ExchangeEntryRecord;
- exchangeDetails: ExchangeDetailsRecord;
-}> {
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
-
- const waiter = createNotificationWaiter(
- ws,
- (notif) =>
- notif.type == NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === exchangeBaseUrl,
- );
-
- const taskId = TaskIdentifiers.forExchangeUpdateFromUrl(exchangeBaseUrl);
-
- while (1) {
- const { exchange, retryRecord } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries])
- .runReadOnly(async (tx) => {
- const exchange = await tx.exchanges.get(exchangeBaseUrl);
- const retryRecord = await tx.operationRetries.get(taskId);
- return { exchange, retryRecord };
- });
-
- if (!exchange) {
- throw Error("exchange does not exist anymore");
- }
-
- switch (exchange.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Ready:
- const details = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- return getExchangeDetails(tx, exchangeBaseUrl);
- });
- if (!details) {
- throw Error("exchange entry inconsistent");
- }
- waiter.cancel();
- return { exchange, exchangeDetails: details };
- case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- case ExchangeEntryDbUpdateStatus.InitialUpdate: {
- waiter.cancel();
- if (retryRecord?.lastError) {
- throw TalerError.fromUncheckedDetail(retryRecord.lastError);
- }
- break;
- }
- case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
- waiter.cancel();
- if (retryRecord?.lastError) {
- throw TalerError.fromUncheckedDetail(retryRecord.lastError);
- } else {
- throw Error(
- "updating exchange failed, error info unavailable (bug!)",
- );
- }
- }
-
- await waiter.waitNext();
- }
- throw Error("not reached");
+export interface ReadyExchangeSummary {
+ exchangeBaseUrl: string;
+ currency: string;
+ masterPub: string;
+ tosStatus: ExchangeTosStatus;
+ tosAcceptedEtag: string | undefined;
+ tosCurrentEtag: string | undefined;
+ wireInfo: WireInfo;
+ protocolVersionRange: string;
+ tosAcceptedTimestamp: TalerPreciseTimestamp | undefined;
}
/**
@@ -834,21 +782,81 @@ export async function fetchFreshExchange(
forceUpdate?: boolean;
expectedMasterPub?: string;
} = {},
-): Promise<{
- exchange: ExchangeEntryRecord;
- exchangeDetails: ExchangeDetailsRecord;
-}> {
+): Promise<ReadyExchangeSummary> {
const canonUrl = canonicalizeBaseUrl(baseUrl);
- await startUpdateExchangeEntry(ws, canonUrl, {
- forceUpdate: options.forceUpdate,
+ const operationId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: canonUrl,
});
- const res = await waitExchangeEntryUpdated(
- ws,
- canonUrl,
- options.cancellationToken,
- );
+
+ const oldExchange = await ws.db
+ .mktx((x) => [x.exchanges])
+ .runReadOnly(async (tx) => {
+ return tx.exchanges.get(canonUrl);
+ });
+
+ let needsUpdate = false;
+
+ if (!oldExchange || options.forceUpdate) {
+ needsUpdate = true;
+ await startUpdateExchangeEntry(ws, canonUrl, {
+ forceUpdate: options.forceUpdate,
+ });
+ } else {
+ const nextUpdate = timestampOptionalAbsoluteFromDb(
+ oldExchange.nextUpdateStamp,
+ );
+ if (nextUpdate == null || AbsoluteTime.isExpired(nextUpdate)) {
+ needsUpdate = true;
+ }
+ }
+
+ if (needsUpdate) {
+ await runTaskWithErrorReporting(ws, operationId, () =>
+ updateExchangeFromUrlHandler(ws, canonUrl),
+ );
+ }
+
+ const { exchange, exchangeDetails } = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadOnly(async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeDetails(tx, canonUrl);
+ return { exchange, exchangeDetails };
+ });
+
+ if (!exchange) {
+ throw Error("exchange entry does not exist anymore");
+ }
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ break;
+ default:
+ throw Error("unable to update exchange");
+ }
+
+ if (!exchangeDetails) {
+ throw Error("invariant failed");
+ }
+
+ const res: ReadyExchangeSummary = {
+ currency: exchangeDetails.currency,
+ exchangeBaseUrl: canonUrl,
+ masterPub: exchangeDetails.masterPublicKey,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosAcceptedEtag: exchange.tosAcceptedEtag,
+ wireInfo: exchangeDetails.wireInfo,
+ protocolVersionRange: exchangeDetails.protocolVersionRange,
+ tosCurrentEtag: exchange.tosCurrentEtag,
+ tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
+ exchange.tosAcceptedTimestamp,
+ ),
+ };
+
if (options.expectedMasterPub) {
- if (res.exchangeDetails.masterPublicKey !== options.expectedMasterPub) {
+ if (res.masterPub !== options.expectedMasterPub) {
throw Error(
"public key of the exchange does not match expected public key",
);
@@ -1187,10 +1195,7 @@ export async function getExchangeTos(
acceptLanguage?: string,
): Promise<GetExchangeTosResult> {
// FIXME: download ToS in acceptable format if passed!
- const { exchange, exchangeDetails } = await fetchFreshExchange(
- ws,
- exchangeBaseUrl,
- );
+ const exch = await fetchFreshExchange(ws, exchangeBaseUrl);
const tosDownload = await downloadTosFromAcceptedFormat(
ws,
@@ -1211,12 +1216,12 @@ export async function getExchangeTos(
});
return {
- acceptedEtag: exchange.tosAcceptedEtag,
+ acceptedEtag: exch.tosAcceptedEtag,
currentEtag: tosDownload.tosEtag,
content: tosDownload.tosText,
contentType: tosDownload.tosContentType,
contentLanguage: tosDownload.tosContentLanguage,
- tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosStatus: exch.tosStatus,
tosAvailableLanguages: tosDownload.tosAvailableLanguages,
};
}
@@ -1364,26 +1369,29 @@ export async function getExchangeDetailedInfo(
const transferFees = Object.entries(
exchange.info.wireInfo.feesForType,
- ).reduce((prev, [wireType, infoForType]) => {
- const feesByGroup = [
- ...infoForType.map((w) => ({
- ...w,
- fee: Amounts.stringify(w.closingFee),
- group: "closing",
- })),
- ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
- ];
- prev[wireType] = createTimeline(
- feesByGroup,
- "sig",
- "startStamp",
- "endStamp",
- "fee",
- "group",
- selectMinimumFee,
- );
- return prev;
- }, {} as Record<string, FeeDescription[]>);
+ ).reduce(
+ (prev, [wireType, infoForType]) => {
+ const feesByGroup = [
+ ...infoForType.map((w) => ({
+ ...w,
+ fee: Amounts.stringify(w.closingFee),
+ group: "closing",
+ })),
+ ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
+ ];
+ prev[wireType] = createTimeline(
+ feesByGroup,
+ "sig",
+ "startStamp",
+ "endStamp",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+ return prev;
+ },
+ {} as Record<string, FeeDescription[]>,
+ );
const globalFeesByGroup = [
...exchange.info.globalFees.map((w) => ({