diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/exchanges.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/exchanges.ts | 164 |
1 files changed, 110 insertions, 54 deletions
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index ca3ea8af6..f983a7c4d 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -27,6 +27,7 @@ import { AbsoluteTime, Amounts, CancellationToken, + DeleteExchangeRequest, DenomKeyType, DenomOperationMap, DenominationInfo, @@ -43,6 +44,7 @@ import { ExchangesListResponse, FeeDescription, GetExchangeEntryByUrlRequest, + GetExchangeResourcesResponse, GetExchangeTosResult, GlobalFees, LibtoolVersion, @@ -63,7 +65,6 @@ import { WireInfo, canonicalizeBaseUrl, codecForExchangeKeysJson, - durationFromSpec, encodeCrock, hashDenomPub, j2s, @@ -86,12 +87,10 @@ import { import { ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, - OpenedPromise, PendingTaskType, WalletDbReadWriteTransaction, createTimeline, isWithdrawableDenom, - openPromise, selectBestForOverlappingDenominations, selectMinimumFee, timestampOptionalAbsoluteFromDb, @@ -100,9 +99,14 @@ import { timestampPreciseToDb, timestampProtocolToDb, } from "../index.js"; -import { CancelFn, InternalWalletState } from "../internal-wallet-state.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; -import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js"; +import { + DbReadOnlyTransaction, + DbReadOnlyTransactionArr, + GetReadOnlyAccess, + GetReadWriteAccess, +} from "../util/query.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js"; import { TaskIdentifiers, @@ -263,14 +267,11 @@ export async function lookupExchangeByUri( } /** - * Mark a ToS version as accepted by the user. - * - * @param etag version of the ToS to accept, or current ToS version of not given + * Mark the current ToS version as accepted by the user. */ export async function acceptExchangeTermsOfService( ws: InternalWalletState, exchangeBaseUrl: string, - etag: string | undefined, ): Promise<void> { const notif = await ws.db .mktx((x) => [x.exchanges, x.exchangeDetails]) @@ -298,6 +299,11 @@ export async function acceptExchangeTermsOfService( } } +/** + * Validate wire fees and wire accounts. + * + * Throw an exception if they are invalid. + */ async function validateWireInfo( ws: InternalWalletState, versionCurrent: number, @@ -364,6 +370,11 @@ async function validateWireInfo( }; } +/** + * Validate global fees. + * + * Throw an exception if they are invalid. + */ async function validateGlobalFees( ws: InternalWalletState, fees: GlobalFees[], @@ -455,7 +466,6 @@ async function provideExchangeRecordInTx( exchangeDetails: typeof WalletStoresV1.exchangeDetails; }>, baseUrl: string, - now: AbsoluteTime, ): Promise<{ exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord | undefined; @@ -655,7 +665,7 @@ async function downloadExchangeKeysInfo( reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay, expiry: AbsoluteTime.toProtocolTimestamp( getExpiry(resp, { - minDuration: durationFromSpec({ hours: 1 }), + minDuration: Duration.fromSpec({ hours: 1 }), }), ), recoup: exchangeKeysJsonUnchecked.recoup ?? [], @@ -718,12 +728,10 @@ export async function startUpdateExchangeEntry( ): Promise<void> { const canonBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl); - const now = AbsoluteTime.now(); - const { notification } = await ws.db .mktx((x) => [x.exchanges, x.exchangeDetails]) .runReadWrite(async (tx) => { - return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, now); + return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl); }); if (notification) { @@ -778,46 +786,6 @@ export async function startUpdateExchangeEntry( ws.workAvailable.trigger(); } -export interface NotificationWaiter { - waitNext(): Promise<void>; - cancel(): void; -} - -export function createNotificationWaiter( - ws: InternalWalletState, - pred: (x: WalletNotification) => boolean, -): NotificationWaiter { - ws.ensureTaskLoopRunning(); - let cancelFn: CancelFn | undefined = undefined; - let p: OpenedPromise<void> | undefined = undefined; - - return { - cancel() { - cancelFn?.(); - }, - waitNext(): Promise<void> { - if (!p) { - p = openPromise(); - cancelFn = ws.addNotificationListener((notif) => { - if (pred(notif)) { - // We got a notification that matches our predicate. - // Resolve promise for existing waiters, - // and create a new promise to wait for the next - // notification occurrence. - const myResolve = p?.resolve; - const myCancel = cancelFn; - p = undefined; - cancelFn = undefined; - myResolve?.(); - myCancel?.(); - } - }); - } - return p.promise; - }, - }; -} - /** * Basic information about an exchange in a ready state. */ @@ -1363,6 +1331,9 @@ export async function listExchanges( * 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.). + * + * The caller should emit the returned notification iff the current transaction + * succeeded. */ export async function markExchangeUsed( ws: InternalWalletState, @@ -1555,3 +1526,88 @@ export async function getExchangeDetailedInfo( }, }; } + +async function internalGetExchangeResources( + ws: InternalWalletState, + tx: DbReadOnlyTransactionArr< + typeof WalletStoresV1, + ["exchanges", "coins", "withdrawalGroups"] + >, + exchangeBaseUrl: string, +): Promise<GetExchangeResourcesResponse> { + let numWithdrawals = 0; + let numCoins = 0; + numCoins = await tx.coins.indexes.byBaseUrl.count(exchangeBaseUrl); + numWithdrawals = + await tx.withdrawalGroups.indexes.byExchangeBaseUrl.count(exchangeBaseUrl); + const total = numWithdrawals + numCoins; + return { + hasResources: total != 0, + }; +} + +export async function deleteExchange( + ws: InternalWalletState, + req: DeleteExchangeRequest, +): Promise<void> { + let inUse: boolean = false; + const exchangeBaseUrl = canonicalizeBaseUrl(req.exchangeBaseUrl); + await ws.db.runReadWriteTx( + ["exchanges", "coins", "withdrawalGroups", "exchangeDetails"], + async (tx) => { + const exchangeRec = await tx.exchanges.get(exchangeBaseUrl); + if (!exchangeRec) { + // Nothing to delete! + logger.info("no exchange found to delete"); + return; + } + const res = await internalGetExchangeResources(ws, tx, exchangeBaseUrl); + if (res.hasResources) { + if (req.purge) { + const detRecs = + await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll(); + for (const r of detRecs) { + if (r.rowId == null) { + // Should never happen, as rowId is the primary key. + continue; + } + await tx.exchangeDetails.delete(r.rowId); + } + // FIXME: Also remove records related to transactions? + } else { + inUse = true; + return; + } + } + await tx.exchanges.delete(exchangeBaseUrl); + }, + ); + + if (inUse) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.WALLET_EXCHANGE_ENTRY_USED, + hint: "Exchange in use.", + }); + } +} + +export async function getExchangeResources( + ws: InternalWalletState, + exchangeBaseUrl: string, +): Promise<GetExchangeResourcesResponse> { + // Withdrawals include internal withdrawals from peer transactions + const res = await ws.db.runReadOnlyTx( + ["exchanges", "withdrawalGroups", "coins"], + async (tx) => { + const exchangeRecord = await tx.exchanges.get(exchangeBaseUrl); + if (!exchangeRecord) { + return undefined; + } + return internalGetExchangeResources(ws, tx, exchangeBaseUrl); + }, + ); + if (!res) { + throw Error("exchange not found"); + } + return res; +} |