summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-16 13:15:40 +0100
committerFlorian Dold <florian@dold.me>2024-01-16 13:15:40 +0100
commit1c286ebb2f1c817f5362517d47466c39826c8699 (patch)
treec0cfcee641529032ee06674b943036fe5c0a7cef /packages/taler-wallet-core
parent2e2cf4049a771c82fcc520686de3ace7603baa05 (diff)
downloadwallet-core-1c286ebb2f1c817f5362517d47466c39826c8699.tar.gz
wallet-core-1c286ebb2f1c817f5362517d47466c39826c8699.tar.bz2
wallet-core-1c286ebb2f1c817f5362517d47466c39826c8699.zip
wallet-core: implement remaining DD48 calls, test
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r--packages/taler-wallet-core/src/db.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts164
-rw-r--r--packages/taler-wallet-core/src/util/query.ts10
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts37
-rw-r--r--packages/taler-wallet-core/src/wallet.ts22
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,
};