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.ts130
1 files changed, 119 insertions, 11 deletions
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;
}