summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/exchanges.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/exchanges.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts164
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;
+}