aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-06-28 14:19:12 +0200
committerFlorian Dold <florian@dold.me>2024-06-28 14:19:12 +0200
commit71f9676fc9d80bccc7353487d8527af74d25a02c (patch)
treee08ae9c3c6652ad6e36eaeb87d6941b1d78bc00f
parentad98210b79f350de631e593ef520f5a57a44812e (diff)
downloadwallet-core-71f9676fc9d80bccc7353487d8527af74d25a02c.tar.xz
wallet-core: return currency specification from exchange
-rw-r--r--packages/taler-util/src/taler-types.ts3
-rw-r--r--packages/taler-util/src/taleruri.ts1
-rw-r--r--packages/taler-util/src/wallet-types.ts18
-rw-r--r--packages/taler-wallet-core/src/db.ts97
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts18
-rw-r--r--packages/taler-wallet-core/src/wallet.ts111
6 files changed, 203 insertions, 45 deletions
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 66f98ea9a..ac42ca278 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -723,6 +723,8 @@ export class ExchangeKeysJson {
currency: string;
+ currency_specification?: CurrencySpecification;
+
/**
* The exchange's master public key.
*/
@@ -1504,6 +1506,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>()
.property("base_url", codecForString())
.property("currency", codecForString())
+ .property("currency_specification", codecOptional(codecForCurrencySpecificiation()))
.property("master_public_key", codecForString())
.property("auditors", codecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index b22dc3c59..d3186d2f5 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -29,6 +29,7 @@ import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AmountString } from "./taler-types.js";
import { URL, URLSearchParams } from "./url.js";
+
/**
* A parsed taler URI.
*/
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 2c92d9295..42c0148e7 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -558,11 +558,13 @@ export enum ScopeType {
}
export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
+
export type ScopeInfoExchange = {
type: ScopeType.Exchange;
currency: string;
url: string;
};
+
export type ScopeInfoAuditor = {
type: ScopeType.Auditor;
currency: string;
@@ -571,6 +573,22 @@ export type ScopeInfoAuditor = {
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+/**
+ * Encode scope info as a string.
+ *
+ * Format must be stable as it's used in the database.
+ */
+export function stringifyScopeInfo(si: ScopeInfo): string {
+ switch (si.type) {
+ case ScopeType.Global:
+ return `taler-si:global/${si.currency}}`;
+ case ScopeType.Auditor:
+ return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`;
+ case ScopeType.Exchange:
+ return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`;
+ }
+}
+
export interface BalancesResponse {
balances: WalletBalance[];
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index d28566910..336ffab67 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -40,6 +40,7 @@ import {
CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DenomLossEventType,
DenomSelectionState,
DenominationInfo,
@@ -51,6 +52,7 @@ import {
HashCodeString,
Logger,
RefreshReason,
+ ScopeInfo,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolDuration,
@@ -61,6 +63,7 @@ import {
WireInfo,
WithdrawalExchangeAccountDetails,
codecForAny,
+ stringifyScopeInfo,
} from "@gnu-taler/taler-util";
import { DbRetryInfo, TaskIdentifiers } from "./common.js";
import {
@@ -2370,6 +2373,23 @@ export interface DenomLossEventRecord {
exchangeBaseUrl: string;
}
+export interface CurrencyInfoRecord {
+ /**
+ * Stringified scope info.
+ */
+ scopeInfoStr: string;
+
+ /**
+ * Currency specification.
+ */
+ currencySpec: CurrencySpecification;
+
+ /**
+ * How did the currency info get set?
+ */
+ source: "exchange" | "user" | "preset";
+}
+
/**
* Schema definition for the IndexedDB
* wallet database.
@@ -2404,6 +2424,11 @@ export const WalletStoresV1 = {
}),
},
}),
+ currencyInfo: describeStoreV2({
+ recordCodec: passthroughCodec<CurrencyInfoRecord>(),
+ storeName: "currencyInfo",
+ keyPath: "scopeInfoStr",
+ }),
globalCurrencyAuditors: describeStoreV2({
recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(),
storeName: "globalCurrencyAuditors",
@@ -3362,3 +3387,75 @@ export async function deleteTalerDatabase(
req.onsuccess = () => resolve();
});
}
+
+/**
+ * High-level helpers to access the database.
+ * Eventually all access to the database should
+ * go through helpers in this namespace.
+ */
+export namespace WalletDbHelpers {
+ export interface GetCurrencyInfoDbResult {
+ /**
+ * Currency specification.
+ */
+ currencySpec: CurrencySpecification;
+
+ /**
+ * How did the currency info get set?
+ */
+ source: "exchange" | "user" | "preset";
+ }
+
+ export interface StoreCurrencyInfoDbRequest {
+ scopeInfo: ScopeInfo;
+ currencySpec: CurrencySpecification;
+ source: "exchange" | "user" | "preset";
+ }
+
+ export async function getCurrencyInfo(
+ tx: WalletDbReadOnlyTransaction<["currencyInfo"]>,
+ scopeInfo: ScopeInfo,
+ ): Promise<GetCurrencyInfoDbResult | undefined> {
+ const s = stringifyScopeInfo(scopeInfo);
+ const res = await tx.currencyInfo.get(s);
+ if (!res) {
+ return undefined;
+ }
+ return {
+ currencySpec: res.currencySpec,
+ source: res.source,
+ };
+ }
+
+ /**
+ * Store currency info for a scope.
+ *
+ * Overrides existing currency infos.
+ */
+ export async function upsertCurrencyInfo(
+ tx: WalletDbReadWriteTransaction<["currencyInfo"]>,
+ req: StoreCurrencyInfoDbRequest,
+ ): Promise<void> {
+ await tx.currencyInfo.put({
+ scopeInfoStr: stringifyScopeInfo(req.scopeInfo),
+ currencySpec: req.currencySpec,
+ source: req.source,
+ });
+ }
+
+ export async function insertCurrencyInfoUnlessExists(
+ tx: WalletDbReadWriteTransaction<["currencyInfo"]>,
+ req: StoreCurrencyInfoDbRequest,
+ ): Promise<void> {
+ const scopeInfoStr = stringifyScopeInfo(req.scopeInfo);
+ const oldRec = await tx.currencyInfo.get(scopeInfoStr);
+ if (oldRec) {
+ return;
+ }
+ await tx.currencyInfo.put({
+ scopeInfoStr: stringifyScopeInfo(req.scopeInfo),
+ currencySpec: req.currencySpec,
+ source: req.source,
+ });
+ }
+}
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index 8fa439715..ab3f95214 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -31,6 +31,7 @@ import {
CancellationToken,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DeleteExchangeRequest,
DenomKeyType,
DenomLossEventType,
@@ -124,6 +125,7 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletStoresV1,
@@ -710,6 +712,7 @@ export interface ExchangeKeysDownloadResult {
globalFees: GlobalFees[];
accounts: ExchangeWireAccount[];
wireFees: { [methodName: string]: WireFeesJson[] };
+ currencySpecification?: CurrencySpecification;
}
/**
@@ -872,6 +875,7 @@ async function downloadExchangeKeysInfo(
globalFees: exchangeKeysJsonUnchecked.global_fees,
accounts: exchangeKeysJsonUnchecked.accounts,
wireFees: exchangeKeysJsonUnchecked.wire_fees,
+ currencySpecification: exchangeKeysJsonUnchecked.currency_specification,
};
}
@@ -1470,6 +1474,7 @@ export async function updateExchangeFromUrlHandler(
"recoupGroups",
"coinAvailability",
"denomLossEvents",
+ "currencyInfo",
],
},
async (tx) => {
@@ -1575,6 +1580,19 @@ export async function updateExchangeFromUrlHandler(
r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
+
+ if (keysInfo.currencySpecification) {
+ await WalletDbHelpers.insertCurrencyInfoUnlessExists(tx, {
+ currencySpec: keysInfo.currencySpecification,
+ scopeInfo: {
+ type: ScopeType.Exchange,
+ currency: newDetails.currency,
+ url: exchangeBaseUrl,
+ },
+ source: "exchange",
+ });
+ }
+
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(
typeof drRowId.key === "number",
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 4e5fdab71..b6167be12 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -38,6 +38,7 @@ import {
DenominationInfo,
Duration,
ExchangesShortListResponse,
+ GetCurrencySpecificationRequest,
GetCurrencySpecificationResponse,
InitResponse,
KnownBankAccounts,
@@ -191,6 +192,7 @@ import {
CoinSourceType,
ConfigRecordKey,
DenominationRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletStoresV1,
clearDatabase,
@@ -758,7 +760,7 @@ async function dispatchRequestInternal(
await fillDefaults(wex);
}
const resp: InitResponse = {
- versionInfo: getVersion(wex),
+ versionInfo: handleGetVersion(wex),
};
if (req.config?.lazyTaskLoop) {
@@ -777,7 +779,7 @@ async function dispatchRequestInternal(
exchangeBaseUrl: "https://exchange.test.taler.net/",
});
return {
- versionInfo: getVersion(wex),
+ versionInfo: handleGetVersion(wex),
};
}
case WalletApiOperation.WithdrawTestBalance: {
@@ -1197,48 +1199,8 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetCurrencySpecification: {
- // Ignore result, just validate in this mock implementation
const req = codecForGetCurrencyInfoRequest().decode(payload);
- // Hard-coded mock for KUDOS and TESTKUDOS
- if (req.scope.currency === "KUDOS") {
- const kudosResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: "Kudos (Taler Demonstrator)",
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2,
- alt_unit_names: {
- "0": "ク",
- },
- },
- };
- return kudosResp;
- } else if (req.scope.currency === "TESTKUDOS") {
- const testkudosResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: "Test (Taler Unstable Demonstrator)",
- num_fractional_input_digits: 0,
- num_fractional_normal_digits: 0,
- num_fractional_trailing_zero_digits: 0,
- alt_unit_names: {
- "0": "テ",
- },
- },
- };
- return testkudosResp;
- }
- const defaultResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: req.scope.currency,
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2,
- alt_unit_names: {
- "0": req.scope.currency,
- },
- },
- };
- return defaultResp;
+ return handleGetCurrencySpecification(wex, req);
}
case WalletApiOperation.ImportBackupRecovery: {
const req = codecForAny().decode(payload);
@@ -1554,7 +1516,7 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.GetVersion: {
- return getVersion(wex);
+ return handleGetVersion(wex);
}
case WalletApiOperation.TestingWaitTransactionsFinal:
return await waitUntilAllTransactionsFinal(wex);
@@ -1626,7 +1588,66 @@ async function dispatchRequestInternal(
);
}
-export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
+export async function handleGetCurrencySpecification(
+ wex: WalletExecutionContext,
+ req: GetCurrencySpecificationRequest,
+): Promise<GetCurrencySpecificationResponse> {
+ const spec = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["currencyInfo"],
+ },
+ async (tx) => {
+ return WalletDbHelpers.getCurrencyInfo(tx, req.scope);
+ },
+ );
+ if (spec) {
+ return {
+ currencySpecification: spec.currencySpec,
+ };
+ }
+ // Hard-coded mock for KUDOS and TESTKUDOS
+ if (req.scope.currency === "KUDOS") {
+ const kudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Kudos (Taler Demonstrator)",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": "ク",
+ },
+ },
+ };
+ return kudosResp;
+ } else if (req.scope.currency === "TESTKUDOS") {
+ const testkudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Test (Taler Unstable Demonstrator)",
+ num_fractional_input_digits: 0,
+ num_fractional_normal_digits: 0,
+ num_fractional_trailing_zero_digits: 0,
+ alt_unit_names: {
+ "0": "テ",
+ },
+ },
+ };
+ return testkudosResp;
+ }
+ const defaultResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: req.scope.currency,
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": req.scope.currency,
+ },
+ },
+ };
+ return defaultResp;
+}
+
+function handleGetVersion(wex: WalletExecutionContext): WalletCoreVersion {
const result: WalletCoreVersion = {
implementationSemver: walletCoreBuildInfo.implementationSemver,
implementationGitHash: walletCoreBuildInfo.implementationGitHash,