diff options
author | Florian Dold <florian@dold.me> | 2024-01-08 21:17:00 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-01-08 21:17:08 +0100 |
commit | 6f2b03021d7946a61d6b8e53dbba7fc10e5f9a4d (patch) | |
tree | 82a3985ab7267e6f4a57c0b275f1929558b5e572 | |
parent | c019f4c040e82baebdbbda8208f10be2fbc19566 (diff) |
wallet-core: exchange management cleanup
10 files changed, 242 insertions, 174 deletions
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index b1389a359..94f2367e1 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -95,29 +95,6 @@ export interface RefreshOperations { ): Promise<RefreshGroupId>; } -/** - * Interface for exchange-related operations. - */ -export interface ExchangeOperations { - // FIXME: Should other operations maybe always use - // updateExchangeFromUrl? - getExchangeDetails( - tx: GetReadOnlyAccess<{ - exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; - }>, - exchangeBaseUrl: string, - ): Promise<ExchangeDetailsRecord | undefined>; - fetchFreshExchange( - ws: InternalWalletState, - baseUrl: string, - options?: { - forceNow?: boolean; - cancellationToken?: CancellationToken; - }, - ): Promise<ReadyExchangeSummary>; -} - export interface RecoupOperations { createRecoupGroup( ws: InternalWalletState, @@ -176,7 +153,6 @@ export interface InternalWalletState { merchantInfoCache: Record<string, MerchantInfo>; - exchangeOps: ExchangeOperations; recoupOps: RecoupOperations; merchantOps: MerchantOperations; refreshOps: RefreshOperations; diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index fdaab0d5f..53ca33fe7 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -49,6 +49,7 @@ /** * Imports. */ +import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { AllowedAuditorInfo, AllowedExchangeInfo, @@ -67,7 +68,6 @@ import { OPERATION_STATUS_ACTIVE_FIRST, OPERATION_STATUS_ACTIVE_LAST, RefreshGroupRecord, - RefreshOperationStatus, WalletStoresV1, WithdrawalGroupStatus, } from "../db.js"; @@ -75,8 +75,7 @@ import { InternalWalletState } from "../internal-wallet-state.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess } from "../util/query.js"; -import { getExchangeDetails } from "./exchanges.js"; -import { GlobalIDB } from "@gnu-taler/idb-bridge"; +import { getExchangeWireDetailsInTx } from "./exchanges.js"; /** * Logger. @@ -516,7 +515,7 @@ export async function getBalanceDetail( .runReadOnly(async (tx) => { const allExchanges = await tx.exchanges.iter().toArray(); for (const e of allExchanges) { - const details = await getExchangeDetails(tx, e.baseUrl); + const details = await getExchangeWireDetailsInTx(tx, e.baseUrl); if (!details || req.currency !== details.currency) { continue; } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 8205b7583..f158d9cf9 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -75,7 +75,6 @@ import { getCandidateWithdrawalDenomsTx, getTotalRefreshCost, timestampPreciseToDb, - timestampProtocolFromDb, timestampProtocolToDb, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -89,7 +88,7 @@ import { runLongpollAsync, spendCoins, } from "./common.js"; -import { getExchangeDetails } from "./exchanges.js"; +import { getExchangeWireDetailsInTx } from "./exchanges.js"; import { extractContractData, generateDepositPermissions, @@ -1168,7 +1167,7 @@ export async function prepareDepositGroup( .runReadOnly(async (tx) => { const allExchanges = await tx.exchanges.iter().toArray(); for (const e of allExchanges) { - const details = await getExchangeDetails(tx, e.baseUrl); + const details = await getExchangeWireDetailsInTx(tx, e.baseUrl); if (!details || amount.currency !== details.currency) { continue; } @@ -1282,7 +1281,7 @@ export async function createDepositGroup( .runReadOnly(async (tx) => { const allExchanges = await tx.exchanges.iter().toArray(); for (const e of allExchanges) { - const details = await getExchangeDetails(tx, e.baseUrl); + const details = await getExchangeWireDetailsInTx(tx, e.baseUrl); if (!details || amount.currency !== details.currency) { continue; } @@ -1495,7 +1494,10 @@ export async function getCounterpartyEffectiveDepositAmount( } for (const exchangeUrl of exchangeSet.values()) { - const exchangeDetails = await getExchangeDetails(tx, exchangeUrl); + const exchangeDetails = await getExchangeWireDetailsInTx( + tx, + exchangeUrl, + ); if (!exchangeDetails) { continue; } @@ -1574,7 +1576,10 @@ async function getTotalFeesForDepositAmount( } for (const exchangeUrl of exchangeSet.values()) { - const exchangeDetails = await getExchangeDetails(tx, exchangeUrl); + const exchangeDetails = await getExchangeWireDetailsInTx( + tx, + exchangeUrl, + ); if (!exchangeDetails) { continue; } 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<ExchangeWireDetails | undefined> { + 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<ExchangeListItem> { + 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<GetExchangeTosResult> { - // 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<ExchangeDetailedResponse> { - // 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; } diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts index 78263c4c3..6b7b62393 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -69,7 +69,7 @@ import { constructTaskIdentifier, runLongpollAsync, } from "./common.js"; -import { fetchFreshExchange } from "./exchanges.js"; +import { fetchFreshExchange, markExchangeUsed } from "./exchanges.js"; import { codecForExchangePurseStatus, getMergeReserveInfo, @@ -82,6 +82,7 @@ import { stopLongpolling, } from "./transactions.js"; import { + PerformCreateWithdrawalGroupResult, getExchangeWithdrawalInfo, internalPerformCreateWithdrawalGroup, internalPrepareCreateWithdrawalGroup, @@ -486,19 +487,20 @@ async function handlePendingMerge( if (!peerInc) { return undefined; } - let withdrawalTransition: TransitionInfo | undefined; const oldTxState = computePeerPushCreditTransactionState(peerInc); + let wgCreateRes: PerformCreateWithdrawalGroupResult | undefined = + undefined; switch (peerInc.status) { case PeerPushCreditStatus.PendingMerge: case PeerPushCreditStatus.PendingMergeKycRequired: { peerInc.status = PeerPushCreditStatus.PendingWithdrawing; - const wgRes = await internalPerformCreateWithdrawalGroup( + wgCreateRes = await internalPerformCreateWithdrawalGroup( ws, tx, withdrawalGroupPrep, ); - withdrawalTransition = wgRes.transitionInfo; - peerInc.withdrawalGroupId = wgRes.withdrawalGroup.withdrawalGroupId; + peerInc.withdrawalGroupId = + wgCreateRes.withdrawalGroup.withdrawalGroupId; break; } } @@ -506,13 +508,17 @@ async function handlePendingMerge( const newTxState = computePeerPushCreditTransactionState(peerInc); return { peerPushCreditTransition: { oldTxState, newTxState }, - withdrawalTransition, + wgCreateRes, }; }); + // Transaction was commited, now we can emit notifications. + if (txRes?.wgCreateRes?.exchangeNotif) { + ws.notify(txRes.wgCreateRes.exchangeNotif); + } notifyTransition( ws, withdrawalGroupPrep.transactionId, - txRes?.withdrawalTransition, + txRes?.wgCreateRes?.transitionInfo, ); notifyTransition(ws, transactionId, txRes?.peerPushCreditTransition); diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 9deb050d8..3a219b39b 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -47,7 +47,6 @@ import { import { DepositElementStatus, DepositGroupRecord, - ExchangeDetailsRecord, OperationRetryRecord, PeerPullCreditRecord, PeerPullDebitRecordStatus, @@ -91,7 +90,10 @@ import { resumeDepositGroup, suspendDepositGroup, } from "./deposits.js"; -import { getExchangeDetails } from "./exchanges.js"; +import { + ExchangeWireDetails, + getExchangeWireDetailsInTx, +} from "./exchanges.js"; import { abortPayMerchant, computePayMerchantTransactionActions, @@ -137,8 +139,8 @@ import { } from "./pay-peer-push-debit.js"; import { iterRecordsForDeposit, - iterRecordsForPeerPullDebit, iterRecordsForPeerPullInitiation as iterRecordsForPeerPullCredit, + iterRecordsForPeerPullDebit, iterRecordsForPeerPushCredit, iterRecordsForPeerPushInitiation as iterRecordsForPeerPushDebit, iterRecordsForPurchase, @@ -240,9 +242,8 @@ export async function getTransactionById( x.operationRetries, ]) .runReadWrite(async (tx) => { - const withdrawalGroupRecord = await tx.withdrawalGroups.get( - withdrawalGroupId, - ); + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); if (!withdrawalGroupRecord) throw Error("not found"); @@ -258,7 +259,7 @@ export async function getTransactionById( ort, ); } - const exchangeDetails = await getExchangeDetails( + const exchangeDetails = await getExchangeWireDetailsInTx( tx, withdrawalGroupRecord.exchangeBaseUrl, ); @@ -290,7 +291,9 @@ export async function getTransactionById( const payOpId = TaskIdentifiers.forPay(purchase); const payRetryRecord = await tx.operationRetries.get(payOpId); - const refunds = await tx.refundGroups.indexes.byProposalId.getAll(purchase.proposalId) + const refunds = await tx.refundGroups.indexes.byProposalId.getAll( + purchase.proposalId, + ); return buildTransactionForPurchase( purchase, @@ -544,7 +547,7 @@ function buildTransactionForPeerPullCredit( const silentWithdrawalErrorForInvoice = wsrOrt?.lastError && wsrOrt.lastError.code === - TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && + TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => { return ( e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR && @@ -574,10 +577,10 @@ function buildTransactionForPeerPullCredit( kycUrl: pullCredit.kycUrl, ...(wsrOrt?.lastError ? { - error: silentWithdrawalErrorForInvoice - ? undefined - : wsrOrt.lastError, - } + error: silentWithdrawalErrorForInvoice + ? undefined + : wsrOrt.lastError, + } : {}), }; } @@ -698,7 +701,7 @@ function buildTransactionForBankIntegratedWithdraw( function buildTransactionForManualWithdraw( withdrawalGroup: WithdrawalGroupRecord, - exchangeDetails: ExchangeDetailsRecord, + exchangeDetails: ExchangeWireDetails, ort?: OperationRetryRecord, ): Transaction { if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) @@ -725,7 +728,8 @@ function buildTransactionForManualWithdraw( type: WithdrawalType.ManualTransfer, reservePub: withdrawalGroup.reservePub, exchangePaytoUris, - exchangeCreditAccountDetails: withdrawalGroup.wgInfo.exchangeCreditAccounts, + exchangeCreditAccountDetails: + withdrawalGroup.wgInfo.exchangeCreditAccounts, reserveIsReady: withdrawalGroup.status === WithdrawalGroupStatus.Done || withdrawalGroup.status === WithdrawalGroupStatus.PendingReady, @@ -928,14 +932,16 @@ async function buildTransactionForPurchase( info.fulfillmentUrl = contractData.fulfillmentUrl; } - const refunds: RefundInfoShort[] = refundsInfo.map(r => ({ + const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({ amountEffective: r.amountEffective, amountRaw: r.amountRaw, - timestamp: TalerPreciseTimestamp.round(timestampPreciseFromDb(r.timestampCreated)), + timestamp: TalerPreciseTimestamp.round( + timestampPreciseFromDb(r.timestampCreated), + ), transactionId: constructTransactionIdentifier({ tag: TransactionType.Refund, refundGroupId: r.refundGroupId, - }) + }), })); const timestamp = purchaseRecord.timestampAccept; @@ -1193,7 +1199,7 @@ export async function getTransactions( ); return; case WithdrawalRecordType.BankManual: { - const exchangeDetails = await getExchangeDetails( + const exchangeDetails = await getExchangeWireDetailsInTx( tx, wsr.exchangeBaseUrl, ); @@ -1258,7 +1264,9 @@ export async function getTransactions( const payOpId = TaskIdentifiers.forPay(purchase); const payRetryRecord = await tx.operationRetries.get(payOpId); - const refunds = await tx.refundGroups.indexes.byProposalId.getAll(purchase.proposalId) + const refunds = await tx.refundGroups.indexes.byProposalId.getAll( + purchase.proposalId, + ); transactions.push( await buildTransactionForPurchase( @@ -1723,9 +1731,8 @@ export async function deleteTransaction( } if (pushInc.withdrawalGroupId) { const withdrawalGroupId = pushInc.withdrawalGroupId; - const withdrawalGroupRecord = await tx.withdrawalGroups.get( - withdrawalGroupId, - ); + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); if (withdrawalGroupRecord) { await tx.withdrawalGroups.delete(withdrawalGroupId); await tx.tombstones.put({ @@ -1753,9 +1760,8 @@ export async function deleteTransaction( } if (pullIni.withdrawalGroupId) { const withdrawalGroupId = pullIni.withdrawalGroupId; - const withdrawalGroupRecord = await tx.withdrawalGroups.get( - withdrawalGroupId, - ); + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); if (withdrawalGroupRecord) { await tx.withdrawalGroups.delete(withdrawalGroupId); await tx.tombstones.put({ @@ -1778,9 +1784,8 @@ export async function deleteTransaction( await ws.db .mktx((x) => [x.withdrawalGroups, x.tombstones]) .runReadWrite(async (tx) => { - const withdrawalGroupRecord = await tx.withdrawalGroups.get( - withdrawalGroupId, - ); + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); if (withdrawalGroupRecord) { await tx.withdrawalGroups.delete(withdrawalGroupId); await tx.tombstones.put({ diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index cf4e9a1d5..49c0e4a14 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -57,6 +57,7 @@ import { TransactionType, URL, UnblindedSignature, + WalletNotification, WithdrawUriInfoResponse, WithdrawalExchangeAccountDetails, addPaytoQueryParams, @@ -133,8 +134,10 @@ import { import { ReadyExchangeSummary, fetchFreshExchange, - getExchangeDetails, getExchangePaytoUri, + getExchangeWireDetailsInTx, + listExchanges, + markExchangeUsed, } from "./exchanges.js"; import { TransitionInfo, @@ -1191,7 +1194,7 @@ export async function updateWithdrawalDenoms( const exchangeDetails = await ws.db .mktx((x) => [x.exchanges, x.exchangeDetails]) .runReadOnly(async (tx) => { - return ws.exchangeOps.getExchangeDetails(tx, exchangeBaseUrl); + return getExchangeWireDetailsInTx(tx, exchangeBaseUrl); }); if (!exchangeDetails) { logger.error("exchange details not available"); @@ -1521,7 +1524,7 @@ async function processWithdrawalGroupPendingReady( withdrawalGroupId, }); - await ws.exchangeOps.fetchFreshExchange(ws, withdrawalGroup.exchangeBaseUrl); + await fetchFreshExchange(ws, withdrawalGroup.exchangeBaseUrl); if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) { logger.warn("Finishing empty withdrawal group (no denoms)"); @@ -1768,7 +1771,7 @@ export async function getExchangeWithdrawalInfo( ageRestricted: number | undefined, ): Promise<ExchangeWithdrawalDetails> { logger.trace("updating exchange"); - const exchange = await ws.exchangeOps.fetchFreshExchange(ws, exchangeBaseUrl); + const exchange = await fetchFreshExchange(ws, exchangeBaseUrl); if (exchange.currency != instructedAmount.currency) { // Specifiying the amount in the conversion input currency is not yet supported. @@ -1917,7 +1920,7 @@ export interface GetWithdrawalDetailsForUriOpts { * Get more information about a taler://withdraw URI. * * As side effects, the bank (via the bank integration API) is queried - * and the exchange suggested by the bank is permanently added + * and the exchange suggested by the bank is ephemerally added * to the wallet's list of known exchanges. */ export async function getWithdrawalDetailsForUri( @@ -1929,10 +1932,10 @@ export async function getWithdrawalDetailsForUri( const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri); logger.trace(`got bank info`); if (info.suggestedExchange) { - // FIXME: right now the exchange gets permanently added, - // we might want to only temporarily add it. try { - await ws.exchangeOps.fetchFreshExchange(ws, info.suggestedExchange); + // If the exchange entry doesn't exist yet, + // it'll be created as an ephemeral entry. + await fetchFreshExchange(ws, info.suggestedExchange); } catch (e) { // We still continued if it failed, as other exchanges might be available. // We don't want to fail if the bank-suggested exchange is broken/offline. @@ -1942,40 +1945,12 @@ export async function getWithdrawalDetailsForUri( } } - // Extract information about possible exchanges for the withdrawal - // operation from the database. - - const exchanges: ExchangeListItem[] = []; - - await ws.db - .mktx((x) => [ - x.exchanges, - x.exchangeDetails, - x.denominations, - x.operationRetries, - ]) - .runReadOnly(async (tx) => { - const exchangeRecords = await tx.exchanges.iter().toArray(); - for (const r of exchangeRecords) { - const exchangeDetails = await ws.exchangeOps.getExchangeDetails( - tx, - r.baseUrl, - ); - const retryRecord = await tx.operationRetries.get( - TaskIdentifiers.forExchangeUpdate(r), - ); - if (exchangeDetails) { - exchanges.push( - makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError), - ); - } - } - }); + const possibleExchangesResp = await listExchanges(ws); return { amount: Amounts.stringify(info.amount), defaultExchangeBaseUrl: info.suggestedExchange, - possibleExchanges: exchanges, + possibleExchanges: possibleExchangesResp.exchanges, }; } @@ -2005,7 +1980,7 @@ export async function getFundingPaytoUris( ): Promise<string[]> { const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); checkDbInvariant(!!withdrawalGroup); - const exchangeDetails = await getExchangeDetails( + const exchangeDetails = await getExchangeWireDetailsInTx( tx, withdrawalGroup.exchangeBaseUrl, ); @@ -2385,7 +2360,7 @@ export async function internalPrepareCreateWithdrawalGroup( wgInfo: args.wgInfo, }; - const exchangeInfo = await fetchFreshExchange(ws, canonExchange); + await fetchFreshExchange(ws, canonExchange); const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, @@ -2404,6 +2379,13 @@ export async function internalPrepareCreateWithdrawalGroup( export interface PerformCreateWithdrawalGroupResult { withdrawalGroup: WithdrawalGroupRecord; transitionInfo: TransitionInfo | undefined; + + /** + * Notification for the exchange state transition. + * + * Should be emitted after the transaction has succeeded. + */ + exchangeNotif: WalletNotification | undefined; } export async function internalPerformCreateWithdrawalGroup( @@ -2417,7 +2399,11 @@ export async function internalPerformCreateWithdrawalGroup( ): Promise<PerformCreateWithdrawalGroupResult> { const { withdrawalGroup } = prep; if (!prep.creationInfo) { - return { withdrawalGroup, transitionInfo: undefined }; + return { + withdrawalGroup, + transitionInfo: undefined, + exchangeNotif: undefined, + }; } await tx.withdrawalGroups.add(withdrawalGroup); await tx.reserves.put({ @@ -2428,7 +2414,6 @@ export async function internalPerformCreateWithdrawalGroup( const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); if (exchange) { exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now()); - exchange.entryStatus = ExchangeEntryDbRecordStatus.Used; await tx.exchanges.put(exchange); } @@ -2442,7 +2427,17 @@ export async function internalPerformCreateWithdrawalGroup( newTxState, }; - return { withdrawalGroup, transitionInfo }; + const exchangeUsedRes = await markExchangeUsed( + ws, + tx, + prep.withdrawalGroup.exchangeBaseUrl, + ); + + return { + withdrawalGroup, + transitionInfo, + exchangeNotif: exchangeUsedRes.notif, + }; } /** @@ -2481,6 +2476,9 @@ export async function internalCreateWithdrawalGroup( .runReadWrite(async (tx) => { return await internalPerformCreateWithdrawalGroup(ws, tx, prep); }); + if (res.exchangeNotif) { + ws.notify(res.exchangeNotif); + } notifyTransition(ws, transactionId, res.transitionInfo); return res.withdrawalGroup; } @@ -2535,10 +2533,7 @@ export async function acceptWithdrawalFromUri( withdrawInfo.wireTypes, ); - const exchange = await ws.exchangeOps.fetchFreshExchange( - ws, - selectedExchange, - ); + const exchange = await fetchFreshExchange(ws, selectedExchange); const withdrawalAccountList = await fetchWithdrawalAccountInfo(ws, { exchange, @@ -2710,7 +2705,7 @@ export async function createManualWithdrawal( ): Promise<AcceptManualWithdrawalResult> { const { exchangeBaseUrl } = req; const amount = Amounts.parseOrThrow(req.amount); - const exchange = await ws.exchangeOps.fetchFreshExchange(ws, exchangeBaseUrl); + const exchange = await fetchFreshExchange(ws, exchangeBaseUrl); if (exchange.currency != amount.currency) { throw Error( diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index e3fbffe98..f24184609 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -57,7 +57,7 @@ import { import { DenominationRecord } from "../db.js"; import { getAutoRefreshExecuteThreshold, - getExchangeDetails, + getExchangeWireDetailsInTx, isWithdrawableDenom, WalletDbReadOnlyTransaction, } from "../index.js"; @@ -615,7 +615,10 @@ async function selectPayMerchantCandidates( const exchanges = await tx.exchanges.iter().toArray(); const wfPerExchange: Record<string, AmountJson> = {}; for (const exchange of exchanges) { - const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl); + const exchangeDetails = await getExchangeWireDetailsInTx( + tx, + exchange.baseUrl, + ); // 1.- exchange has same currency if (exchangeDetails?.currency !== req.contractTermsAmount.currency) { continue; diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts index 4365e6d32..caa3fdca5 100644 --- a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts +++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts @@ -33,7 +33,7 @@ import { import { DenominationRecord, InternalWalletState, - getExchangeDetails, + getExchangeWireDetailsInTx, timestampProtocolFromDb, } from "../index.js"; import { CoinInfo } from "./coinSelection.js"; @@ -61,8 +61,8 @@ function getOperationType(txType: TransactionType): OperationType { txType === TransactionType.Withdrawal ? OperationType.Credit : txType === TransactionType.Deposit - ? OperationType.Debit - : undefined; + ? OperationType.Debit + : undefined; if (!operationType) { throw Error(`operation type ${txType} not yet supported`); } @@ -155,7 +155,10 @@ async function getAvailableDenoms( filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl); for (const exchangeBaseUrl of filteredExchanges) { - const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl); + const exchangeDetails = await getExchangeWireDetailsInTx( + tx, + exchangeBaseUrl, + ); // 1.- exchange has same currency if (exchangeDetails?.currency !== currency) { continue; @@ -221,9 +224,10 @@ async function getAvailableDenoms( //4.- filter coins restricted by age if (operationType === OperationType.Credit) { // FIXME: Use denom groups instead of querying all denominations! - const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll( - exchangeBaseUrl, - ); + const ds = + await tx.denominations.indexes.byExchangeBaseUrl.getAll( + exchangeBaseUrl, + ); for (const denom of ds) { const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp( timestampProtocolFromDb(denom.stampExpireWithdraw), diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 154665c47..ff1f991dd 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -41,7 +41,6 @@ import { KnownBankAccounts, KnownBankAccountsInfo, Logger, - WithdrawalDetailsForAmount, MerchantUsingTemplateDetails, NotificationType, PrepareWithdrawExchangeRequest, @@ -61,6 +60,7 @@ import { ValidateIbanResponse, WalletCoreVersion, WalletNotification, + WithdrawalDetailsForAmount, codecForAbortTransaction, codecForAcceptBankIntegratedWithdrawalRequest, codecForAcceptExchangeTosRequest, @@ -157,7 +157,6 @@ import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js"; import { ActiveLongpollInfo, CancelFn, - ExchangeOperations, InternalWalletState, MerchantInfo, MerchantOperations, @@ -185,10 +184,8 @@ import { } from "./operations/backup/index.js"; import { getBalanceDetail, getBalances } from "./operations/balance.js"; import { - TaskIdentifiers, TaskRunResult, TaskRunResultType, - makeExchangeListItem, runTaskWithErrorReporting, } from "./operations/common.js"; import { @@ -203,9 +200,9 @@ import { addPresetExchangeEntry, fetchFreshExchange, getExchangeDetailedInfo, - getExchangeDetails, getExchangeTos, listExchanges, + lookupExchangeByUri, updateExchangeFromUrlHandler, } from "./operations/exchanges.js"; import { getMerchantInfo } from "./operations/merchants.js"; @@ -967,32 +964,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( } case WalletApiOperation.GetExchangeEntryByUrl: { const req = codecForGetExchangeEntryByUrlRequest().decode(payload); - const exchangeEntry = 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 getExchangeDetails( - tx, - exchangeRec.baseUrl, - ); - const opRetryRecord = await tx.operationRetries.get( - TaskIdentifiers.forExchangeUpdate(exchangeRec), - ); - return makeExchangeListItem( - exchangeRec, - exchangeDetails, - opRetryRecord?.lastError, - ); - }); - return exchangeEntry; + return lookupExchangeByUri(ws, req); } case WalletApiOperation.ListExchangesForScopedCurrency: { const req = @@ -1687,11 +1659,6 @@ class InternalWalletStateImpl implements InternalWalletState { initCalled = false; - exchangeOps: ExchangeOperations = { - getExchangeDetails, - fetchFreshExchange, - }; - recoupOps: RecoupOperations = { createRecoupGroup, }; |