diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/exchanges.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/exchanges.ts | 224 |
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) => ({ |