From 6f2b03021d7946a61d6b8e53dbba7fc10e5f9a4d Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 8 Jan 2024 21:17:00 +0100 Subject: wallet-core: exchange management cleanup --- .../taler-wallet-core/src/operations/exchanges.ts | 130 +++++++++++++++++++-- 1 file changed, 119 insertions(+), 11 deletions(-) (limited to 'packages/taler-wallet-core/src/operations/exchanges.ts') diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 67d598e70..766af27a8 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import { DenominationInfo, DenominationPubKey, Duration, + EddsaPublicKeyString, ExchangeAuditor, ExchangeDetailedResponse, ExchangeGlobalFees, @@ -41,6 +42,7 @@ import { ExchangeWireAccount, ExchangesListResponse, FeeDescription, + GetExchangeEntryByUrlRequest, GetExchangeTosResult, GlobalFees, LibtoolVersion, @@ -175,10 +177,8 @@ async function downloadExchangeWithTermsOfService( /** * Get exchange details from the database. - * - * FIXME: Should we encapsulate the result better, instead of returning the raw DB records here? */ -export async function getExchangeDetails( +async function getExchangeRecordsInternal( tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; exchangeDetails: typeof WalletStoresV1.exchangeDetails; @@ -201,6 +201,67 @@ export async function getExchangeDetails( ]); } +export interface ExchangeWireDetails { + currency: string; + masterPublicKey: EddsaPublicKeyString; + wireInfo: WireInfo; + exchangeBaseUrl: string; + auditors: ExchangeAuditor[]; + globalFees: ExchangeGlobalFees[]; +} + +export async function getExchangeWireDetailsInTx( + tx: GetReadOnlyAccess<{ + exchanges: typeof WalletStoresV1.exchanges; + exchangeDetails: typeof WalletStoresV1.exchangeDetails; + }>, + exchangeBaseUrl: string, +): Promise { + const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl); + if (!det) { + return undefined; + } + return { + currency: det.currency, + masterPublicKey: det.masterPublicKey, + wireInfo: det.wireInfo, + exchangeBaseUrl: det.exchangeBaseUrl, + auditors: det.auditors, + globalFees: det.globalFees, + }; +} + +export async function lookupExchangeByUri( + ws: InternalWalletState, + req: GetExchangeEntryByUrlRequest, +): Promise { + return await ws.db + .mktx((x) => [ + x.exchanges, + x.exchangeDetails, + x.denominations, + x.operationRetries, + ]) + .runReadOnly(async (tx) => { + const exchangeRec = await tx.exchanges.get(req.exchangeBaseUrl); + if (!exchangeRec) { + throw Error("exchange not found"); + } + const exchangeDetails = await getExchangeRecordsInternal( + tx, + exchangeRec.baseUrl, + ); + const opRetryRecord = await tx.operationRetries.get( + TaskIdentifiers.forExchangeUpdate(exchangeRec), + ); + return makeExchangeListItem( + exchangeRec, + exchangeDetails, + opRetryRecord?.lastError, + ); + }); +} + /** * Mark a ToS version as accepted by the user. * @@ -417,7 +478,7 @@ async function provideExchangeRecordInTx( newExchangeState: getExchangeState(r), }; } - const exchangeDetails = await getExchangeDetails(tx, baseUrl); + const exchangeDetails = await getExchangeRecordsInternal(tx, baseUrl); return { exchange, exchangeDetails, notification }; } @@ -825,7 +886,7 @@ export async function fetchFreshExchange( .mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries]) .runReadOnly(async (tx) => { const exchange = await tx.exchanges.get(canonUrl); - const exchangeDetails = await getExchangeDetails(tx, canonUrl); + const exchangeDetails = await getExchangeRecordsInternal(tx, canonUrl); const retryInfo = await tx.operationRetries.get(operationId); return { exchange, exchangeDetails, retryInfo }; }); @@ -980,7 +1041,7 @@ export async function updateExchangeFromUrlHandler( return; } const oldExchangeState = getExchangeState(r); - const existingDetails = await getExchangeDetails(tx, r.baseUrl); + const existingDetails = await getExchangeRecordsInternal(tx, r.baseUrl); if (!existingDetails) { detailsPointerChanged = true; } @@ -1173,7 +1234,7 @@ export async function getExchangePaytoUri( const details = await ws.db .mktx((x) => [x.exchangeDetails, x.exchanges]) .runReadOnly(async (tx) => { - return getExchangeDetails(tx, exchangeBaseUrl); + return getExchangeRecordsInternal(tx, exchangeBaseUrl); }); const accounts = details?.wireInfo.accounts ?? []; for (const account of accounts) { @@ -1202,7 +1263,6 @@ export async function getExchangeTos( acceptedFormat?: string[], acceptLanguage?: string, ): Promise { - // FIXME: download ToS in acceptable format if passed! const exch = await fetchFreshExchange(ws, exchangeBaseUrl); const tosDownload = await downloadTosFromAcceptedFormat( @@ -1234,6 +1294,10 @@ export async function getExchangeTos( }; } +/** + * Parsed information about an exchange, + * obtained by requesting /keys. + */ export interface ExchangeInfo { keys: ExchangeKeysDownloadResult; } @@ -1273,7 +1337,7 @@ export async function listExchanges( tag: PendingTaskType.ExchangeUpdate, exchangeBaseUrl: r.baseUrl, }); - const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); + const exchangeDetails = await getExchangeRecordsInternal(tx, r.baseUrl); const opRetryRecord = await tx.operationRetries.get(taskId); exchanges.push( makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError), @@ -1283,11 +1347,55 @@ export async function listExchanges( return { exchanges }; } +/** + * Transition an exchange to the "used" entry state if necessary. + * + * Should be called whenever the exchange is actively used by the client (for withdrawals etc.). + */ +export async function markExchangeUsed( + ws: InternalWalletState, + tx: GetReadWriteAccess<{ exchanges: typeof WalletStoresV1.exchanges }>, + exchangeBaseUrl: string, +): Promise<{ notif: WalletNotification | undefined }> { + exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl); + logger.info(`marking exchange ${exchangeBaseUrl} as used`); + const exch = await tx.exchanges.get(exchangeBaseUrl); + if (!exch) { + return { + notif: undefined, + }; + } + const oldExchangeState = getExchangeState(exch); + switch (exch.entryStatus) { + case ExchangeEntryDbRecordStatus.Ephemeral: + case ExchangeEntryDbRecordStatus.Preset: { + exch.entryStatus = ExchangeEntryDbRecordStatus.Used; + await tx.exchanges.put(exch); + const newExchangeState = getExchangeState(exch); + return { + notif: { + type: NotificationType.ExchangeStateTransition, + exchangeBaseUrl, + newExchangeState: newExchangeState, + oldExchangeState: oldExchangeState, + } satisfies WalletNotification, + }; + } + default: + return { + notif: undefined, + }; + } +} + +/** + * Get detailed information about the exchange including a timeline + * for the fees charged by the exchange. + */ export async function getExchangeDetailedInfo( ws: InternalWalletState, exchangeBaseurl: string, ): Promise { - // TODO: should we use the forceUpdate parameter? const exchange = await ws.db .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations]) .runReadOnly(async (tx) => { @@ -1297,7 +1405,7 @@ export async function getExchangeDetailedInfo( return; } const { currency } = dp; - const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl); + const exchangeDetails = await getExchangeRecordsInternal(tx, ex.baseUrl); if (!exchangeDetails) { return; } -- cgit v1.2.3