diff options
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 8 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/exchanges.ts | 164 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/query.ts | 10 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet-api-types.ts | 37 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 22 |
5 files changed, 175 insertions, 66 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 5a412fb27..73739c19c 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -149,7 +149,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 1; +export const WALLET_DB_MINOR_VERSION = 2; declare const symDbProtocolTimestamp: unique symbol; @@ -2383,6 +2383,9 @@ export const WalletStoresV1 = { autoIncrement: true, }), { + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 2, + }), byPointer: describeIndex( "byDetailsPointer", ["exchangeBaseUrl", "currency", "masterPublicKey"], @@ -2461,6 +2464,9 @@ export const WalletStoresV1 = { }), { byStatus: describeIndex("byStatus", "status"), + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 2, + }), byTalerWithdrawUri: describeIndex( "byTalerWithdrawUri", "wgInfo.bankInfo.talerWithdrawUri", 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; +} diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 17b9b407c..59c6ea2f5 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -344,6 +344,7 @@ interface IndexReadOnlyAccessor<RecordType> { query?: IDBKeyRange | IDBValidKey, count?: number, ): Promise<RecordType[]>; + count(query?: IDBValidKey): Promise<number>; } type GetIndexReadOnlyAccess<RecordType, IndexMap> = { @@ -357,6 +358,7 @@ interface IndexReadWriteAccessor<RecordType> { query?: IDBKeyRange | IDBValidKey, count?: number, ): Promise<RecordType[]>; + count(query?: IDBValidKey): Promise<number>; } type GetIndexReadWriteAccess<RecordType, IndexMap> = { @@ -696,6 +698,10 @@ function makeReadContext( .getAll(query, count); return requestToPromise(req); }, + count(query) { + const req = tx.objectStore(storeName).index(indexName).count(query); + return requestToPromise(req); + }, }; } ctx[storeAlias] = { @@ -745,6 +751,10 @@ function makeWriteContext( .getAll(query, count); return requestToPromise(req); }, + count(query) { + const req = tx.objectStore(storeName).index(indexName).count(query); + return requestToPromise(req); + }, }; } ctx[storeAlias] = { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 7d3dc86a3..2cbf693f5 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -130,6 +130,9 @@ import { WithdrawUriInfoResponse, WithdrawalTransactionByURIRequest, TransactionWithdrawal, + GetExchangeResourcesRequest, + DeleteExchangeRequest, + GetExchangeResourcesResponse, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, @@ -238,6 +241,8 @@ export enum WalletApiOperation { ListExchangesForScopedCurrency = "listExchangesForScopedCurrency", PrepareWithdrawExchange = "prepareWithdrawExchange", TestingInfiniteTransactionLoop = "testingInfiniteTransactionLoop", + GetExchangeResources = "getExchangeResources", + DeleteExchange = "deleteExchange", } // group: Initialization @@ -668,6 +673,24 @@ export type GetExchangeEntryByUrlOp = { }; /** + * Get resources associated with an exchange. + */ +export type GetExchangeResourcesOp = { + op: WalletApiOperation.GetExchangeResources; + request: GetExchangeResourcesRequest; + response: GetExchangeResourcesResponse; +}; + +/** + * Get resources associated with an exchange. + */ +export type DeleteExchangeOp = { + op: WalletApiOperation.GetExchangeResources; + request: DeleteExchangeRequest; + response: EmptyObject; +}; + +/** * List currencies known to the wallet. */ export type ListCurrenciesOp = { @@ -1206,6 +1229,8 @@ export type WalletOperations = { [WalletApiOperation.UpdateExchangeEntry]: UpdateExchangeEntryOp; [WalletApiOperation.PrepareWithdrawExchange]: PrepareWithdrawExchangeOp; [WalletApiOperation.TestingInfiniteTransactionLoop]: any; + [WalletApiOperation.DeleteExchange]: DeleteExchangeOp; + [WalletApiOperation.GetExchangeResources]: GetExchangeResourcesOp; }; export type WalletCoreRequestType< @@ -1229,10 +1254,10 @@ type Primitives = string | number | boolean; type RecursivePartial<T extends object> = { [P in keyof T]?: T[P] extends Array<infer U extends object> - ? Array<RecursivePartial<U>> - : T[P] extends Array<infer J extends Primitives> - ? Array<J> - : T[P] extends object - ? RecursivePartial<T[P]> - : T[P]; + ? Array<RecursivePartial<U>> + : T[P] extends Array<infer J extends Primitives> + ? Array<J> + : T[P] extends object + ? RecursivePartial<T[P]> + : T[P]; } & object; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 1a876b2c8..3294e2a09 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -73,6 +73,7 @@ import { codecForConfirmPeerPushPaymentRequest, codecForConvertAmountRequest, codecForCreateDepositGroupRequest, + codecForDeleteExchangeRequest, codecForDeleteStoredBackupRequest, codecForDeleteTransactionRequest, codecForFailTransactionRequest, @@ -83,6 +84,7 @@ import { codecForGetContractTermsDetails, codecForGetCurrencyInfoRequest, codecForGetExchangeEntryByUrlRequest, + codecForGetExchangeResourcesRequest, codecForGetExchangeTosRequest, codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, @@ -189,8 +191,10 @@ import { import { acceptExchangeTermsOfService, addPresetExchangeEntry, + deleteExchange, fetchFreshExchange, getExchangeDetailedInfo, + getExchangeResources, getExchangeTos, listExchanges, lookupExchangeByUri, @@ -726,9 +730,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } @@ -1071,7 +1075,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( } case WalletApiOperation.SetExchangeTosAccepted: { const req = codecForAcceptExchangeTosRequest().decode(payload); - await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag); + await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl); return {}; } case WalletApiOperation.AcceptBankIntegratedWithdrawal: { @@ -1400,6 +1404,15 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( ws.workAvailable.trigger(); return {}; } + case WalletApiOperation.DeleteExchange: { + const req = codecForDeleteExchangeRequest().decode(payload); + await deleteExchange(ws, req); + return {}; + } + case WalletApiOperation.GetExchangeResources: { + const req = codecForGetExchangeResourcesRequest().decode(payload); + return await getExchangeResources(ws, req.exchangeBaseUrl); + } case WalletApiOperation.TestingInfiniteTransactionLoop: { const myDelayMs = (payload as any).delayMs ?? 5; const shouldFetch = !!(payload as any).shouldFetch; @@ -1616,7 +1629,6 @@ class InternalWalletStateImpl implements InternalWalletState { createRecoupGroup, }; - refreshOps: RefreshOperations = { createRefreshGroup, }; |