From 71f9676fc9d80bccc7353487d8527af74d25a02c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 28 Jun 2024 14:19:12 +0200 Subject: wallet-core: return currency specification from exchange --- packages/taler-util/src/taler-types.ts | 3 + packages/taler-util/src/taleruri.ts | 1 + packages/taler-util/src/wallet-types.ts | 18 +++++ packages/taler-wallet-core/src/db.ts | 97 ++++++++++++++++++++++++ packages/taler-wallet-core/src/exchanges.ts | 18 +++++ packages/taler-wallet-core/src/wallet.ts | 111 +++++++++++++++++----------- 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 => buildCodecForObject() .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(), + storeName: "currencyInfo", + keyPath: "scopeInfoStr", + }), globalCurrencyAuditors: describeStoreV2({ recordCodec: passthroughCodec(), 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 { + 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 { + await tx.currencyInfo.put({ + scopeInfoStr: stringifyScopeInfo(req.scopeInfo), + currencySpec: req.currencySpec, + source: req.source, + }); + } + + export async function insertCurrencyInfoUnlessExists( + tx: WalletDbReadWriteTransaction<["currencyInfo"]>, + req: StoreCurrencyInfoDbRequest, + ): Promise { + 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 { + 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, -- cgit v1.2.3