aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-06-02 13:23:51 +0200
committerFlorian Dold <florian@dold.me>2021-06-02 13:24:28 +0200
commit02f1d4b08116c24f0af1f32cb6d82be292fa6d10 (patch)
tree1fbcc1675e09584a74896909e1d4d0882d10be8e
parentc6c17a1c0aaa2c76616ec93df3ebe6621b547cd9 (diff)
support multiple exchange details per base URL
-rw-r--r--packages/taler-util/src/backupTypes.ts34
-rw-r--r--packages/taler-wallet-cli/src/index.ts4
-rw-r--r--packages/taler-wallet-core/src/db.ts135
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts53
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts116
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/currencies.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts14
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts627
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts21
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts36
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts49
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts25
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts11
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts91
-rw-r--r--packages/taler-wallet-core/src/wallet.ts63
17 files changed, 699 insertions, 596 deletions
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
index ce2eb6b77..dc344ee23 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -128,6 +128,8 @@ export interface WalletBackupContentV1 {
*/
exchanges: BackupExchange[];
+ exchange_details: BackupExchangeDetails[];
+
/**
* Grouped refresh sessions.
*
@@ -1090,9 +1092,34 @@ export class BackupExchangeAuditor {
}
/**
- * Backup information about an exchange.
+ * Backup information for an exchange. Serves effectively
+ * as a pointer to the exchange details identified by
+ * the base URL, master public key and currency.
*/
export interface BackupExchange {
+ base_url: string;
+
+ master_public_key: string;
+
+ currency: string;
+
+ /**
+ * Time when the pointer to the exchange details
+ * was last updated.
+ *
+ * Used to facilitate automatic merging.
+ */
+ update_clock: Timestamp;
+}
+
+/**
+ * Backup information about an exchange's details.
+ *
+ * Note that one base URL can have multiple exchange
+ * details. The BackupExchange stores a pointer
+ * to the current exchange details.
+ */
+export interface BackupExchangeDetails {
/**
* Canonicalized base url of the exchange.
*/
@@ -1158,11 +1185,6 @@ export interface BackupExchange {
* ETag for last terms of service download.
*/
tos_etag_accepted: string | undefined;
-
- /**
- * Should this exchange be considered defective?
- */
- defective?: boolean;
}
export enum BackupProposalStatus {
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index aad034b87..db6c0a9f3 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -598,10 +598,10 @@ advancedCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const exchange = await wallet.updateExchangeFromUrl(
+ const { exchange, exchangeDetails } = await wallet.updateExchangeFromUrl(
args.withdrawManually.exchange,
);
- const acct = exchange.wireInfo?.accounts[0];
+ const acct = exchangeDetails.wireInfo.accounts[0];
if (!acct) {
console.log("exchange has no accounts");
return;
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 0ff34d3c7..c457d0ffc 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -513,20 +513,32 @@ export interface DenominationRecord {
exchangeBaseUrl: string;
}
-/**
- * Details about the exchange that we only know after
- * querying /keys and /wire.
- */
-export interface ExchangeDetails {
+export enum ExchangeUpdateStatus {
+ FetchKeys = "fetch-keys",
+ FetchWire = "fetch-wire",
+ FetchTerms = "fetch-terms",
+ FinalizeUpdate = "finalize-update",
+ Finished = "finished",
+}
+
+export interface ExchangeBankAccount {
+ payto_uri: string;
+ master_sig: string;
+}
+
+export enum ExchangeUpdateReason {
+ Initial = "initial",
+ Forced = "forced",
+ Scheduled = "scheduled",
+}
+
+export interface ExchangeDetailsRecord {
/**
* Master public key of the exchange.
*/
masterPublicKey: string;
- /**
- * Auditors (partially) auditing the exchange.
- */
- auditors: Auditor[];
+ exchangeBaseUrl: string;
/**
* Currency that the exchange offers.
@@ -534,6 +546,11 @@ export interface ExchangeDetails {
currency: string;
/**
+ * Auditors (partially) auditing the exchange.
+ */
+ auditors: Auditor[];
+
+ /**
* Last observed protocol version.
*/
protocolVersion: string;
@@ -547,6 +564,23 @@ export interface ExchangeDetails {
signingKeys: ExchangeSignKeyJson[];
/**
+ * Terms of service text or undefined if not downloaded yet.
+ *
+ * This is just used as a cache of the last downloaded ToS.
+ */
+ termsOfServiceText: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceLastEtag: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceAcceptedEtag: string | undefined;
+
+ /**
* Timestamp for last update.
*/
lastUpdateTime: Timestamp;
@@ -555,30 +589,25 @@ export interface ExchangeDetails {
* When should we next update the information about the exchange?
*/
nextUpdateTime: Timestamp;
-}
-export enum ExchangeUpdateStatus {
- FetchKeys = "fetch-keys",
- FetchWire = "fetch-wire",
- FetchTerms = "fetch-terms",
- FinalizeUpdate = "finalize-update",
- Finished = "finished",
+ wireInfo: WireInfo;
}
-export interface ExchangeBankAccount {
- payto_uri: string;
- master_sig: string;
-}
-
-export interface ExchangeWireInfo {
+export interface WireInfo {
feesForType: { [wireMethod: string]: WireFee[] };
+
accounts: ExchangeBankAccount[];
}
-export enum ExchangeUpdateReason {
- Initial = "initial",
- Forced = "forced",
- Scheduled = "scheduled",
+export interface ExchangeDetailsPointer {
+ masterPublicKey: string;
+ currency: string;
+
+ /**
+ * Timestamp when the (masterPublicKey, currency) pointer
+ * has been updated.
+ */
+ updateClock: Timestamp;
}
/**
@@ -590,10 +619,7 @@ export interface ExchangeRecord {
*/
baseUrl: string;
- /**
- * Did we finish adding the exchange?
- */
- addComplete: boolean;
+ detailsPointer: ExchangeDetailsPointer | undefined;
/**
* Is this a permanent or temporary exchange record?
@@ -601,38 +627,6 @@ export interface ExchangeRecord {
permanent: boolean;
/**
- * Was the exchange added as a built-in exchange?
- */
- builtIn: boolean;
-
- /**
- * Details, once known.
- */
- details: ExchangeDetails | undefined;
-
- /**
- * Mapping from wire method type to the wire fee.
- */
- wireInfo: ExchangeWireInfo | undefined;
-
- /**
- * Terms of service text or undefined if not downloaded yet.
- *
- * This is just used as a cache of the last downloaded ToS.
- */
- termsOfServiceText: string | undefined;
-
- /**
- * ETag for last terms of service download.
- */
- termsOfServiceLastEtag: string | undefined;
-
- /**
- * ETag for last terms of service download.
- */
- termsOfServiceAcceptedEtag: string | undefined;
-
- /**
* Time when the update to the exchange has been started or
* undefined if no update is in progress.
*/
@@ -640,6 +634,9 @@ export interface ExchangeRecord {
/**
* Status of updating the info about the exchange.
+ *
+ * FIXME: Adapt this to recent changes regarding how
+ * updating exchange details works.
*/
updateStatus: ExchangeUpdateStatus;
@@ -1548,7 +1545,7 @@ export interface BackupProviderTerms {
export interface BackupProviderRecord {
/**
* Base URL of the provider.
- *
+ *
* Primary key for the record.
*/
baseUrl: string;
@@ -1692,6 +1689,17 @@ class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
}
}
+class ExchangeDetailsStore extends Store<
+ "exchangeDetails",
+ ExchangeDetailsRecord
+> {
+ constructor() {
+ super("exchangeDetails", {
+ keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
+ });
+ }
+}
+
class CoinsStore extends Store<"coins", CoinRecord> {
constructor() {
super("coins", { keyPath: "coinPub" });
@@ -1924,6 +1932,7 @@ export const Stores = {
exchangeTrustStore: new ExchangeTrustStore(),
denominations: new DenominationsStore(),
exchanges: new ExchangesStore(),
+ exchangeDetails: new ExchangeDetailsStore(),
proposals: new ProposalsStore(),
refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
"refreshGroups",
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index 70d249ab8..fa0af1b07 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -47,6 +47,7 @@ import {
BackupProposalStatus,
BackupRefreshOldCoin,
BackupRefreshSession,
+ BackupExchangeDetails,
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../state";
import {
@@ -65,6 +66,7 @@ import {
} from "../../db.js";
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
+import { getExchangeDetails } from "../exchanges.js";
export async function exportBackup(
ws: InternalWalletState,
@@ -74,6 +76,7 @@ export async function exportBackup(
[
Stores.config,
Stores.exchanges,
+ Stores.exchangeDetails,
Stores.coins,
Stores.denominations,
Stores.purchases,
@@ -88,6 +91,7 @@ export async function exportBackup(
async (tx) => {
const bs = await getWalletBackupState(ws, tx);
+ const backupExchangeDetails: BackupExchangeDetails[] = [];
const backupExchanges: BackupExchange[] = [];
const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
const backupDenominationsByExchange: {
@@ -254,21 +258,22 @@ export async function exportBackup(
});
});
- await tx.iter(Stores.exchanges).forEach((ex) => {
- // Only back up permanently added exchanges.
-
- if (!ex.details) {
- return;
- }
- if (!ex.wireInfo) {
- return;
- }
- if (!ex.addComplete) {
- return;
- }
- if (!ex.permanent) {
+ await tx.iter(Stores.exchanges).forEachAsync(async (ex) => {
+ const dp = ex.detailsPointer;
+ if (!dp) {
return;
}
+ backupExchanges.push({
+ base_url: ex.baseUrl,
+ currency: dp.currency,
+ master_public_key: dp.masterPublicKey,
+ update_clock: dp.updateClock,
+ });
+ });
+
+ await tx.iter(Stores.exchangeDetails).forEachAsync(async (ex) => {
+ // Only back up permanently added exchanges.
+
const wi = ex.wireInfo;
const wireFees: BackupExchangeWireFee[] = [];
@@ -285,23 +290,23 @@ export async function exportBackup(
}
});
- backupExchanges.push({
- base_url: ex.baseUrl,
- reserve_closing_delay: ex.details.reserveClosingDelay,
+ backupExchangeDetails.push({
+ base_url: ex.exchangeBaseUrl,
+ reserve_closing_delay: ex.reserveClosingDelay,
accounts: ex.wireInfo.accounts.map((x) => ({
payto_uri: x.payto_uri,
master_sig: x.master_sig,
})),
- auditors: ex.details.auditors.map((x) => ({
+ auditors: ex.auditors.map((x) => ({
auditor_pub: x.auditor_pub,
auditor_url: x.auditor_url,
denomination_keys: x.denomination_keys,
})),
- master_public_key: ex.details.masterPublicKey,
- currency: ex.details.currency,
- protocol_version: ex.details.protocolVersion,
+ master_public_key: ex.masterPublicKey,
+ currency: ex.currency,
+ protocol_version: ex.protocolVersion,
wire_fees: wireFees,
- signing_keys: ex.details.signingKeys.map((x) => ({
+ signing_keys: ex.signingKeys.map((x) => ({
key: x.key,
master_sig: x.master_sig,
stamp_end: x.stamp_end,
@@ -310,8 +315,9 @@ export async function exportBackup(
})),
tos_etag_accepted: ex.termsOfServiceAcceptedEtag,
tos_etag_last: ex.termsOfServiceLastEtag,
- denominations: backupDenominationsByExchange[ex.baseUrl] ?? [],
- reserves: backupReservesByExchange[ex.baseUrl] ?? [],
+ denominations:
+ backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
+ reserves: backupReservesByExchange[ex.exchangeBaseUrl] ?? [],
});
});
@@ -451,6 +457,7 @@ export async function exportBackup(
schema_id: "gnu-taler-wallet-backup-content",
schema_version: 1,
exchanges: backupExchanges,
+ exchange_details: backupExchangeDetails,
wallet_root_pub: bs.walletRootPub,
backup_providers: backupBackupProviders,
current_device_id: bs.deviceId,
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 1bbba6e26..f0a944a22 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -32,7 +32,6 @@ import {
Stores,
WalletContractData,
DenomSelectionState,
- ExchangeWireInfo,
ExchangeUpdateStatus,
DenominationStatus,
CoinSource,
@@ -46,6 +45,7 @@ import {
RefundState,
AbortStatus,
RefreshSessionRecord,
+ WireInfo,
} from "../../db.js";
import { TransactionHandle } from "../../index.js";
import { PayCoinSelection } from "../../util/coinSelection";
@@ -56,6 +56,7 @@ import { initRetryInfo } from "../../util/retries";
import { InternalWalletState } from "../state";
import { provideBackupState } from "./state";
import { makeEventId, TombstoneTag } from "../transactions.js";
+import { getExchangeDetails } from "../exchanges.js";
const logger = new Logger("operations/backup/import.ts");
@@ -102,13 +103,13 @@ async function recoverPayCoinSelection(
totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
- const exchange = await tx.get(
- Stores.exchanges,
+ const exchangeDetails = await getExchangeDetails(
+ tx,
coinRecord.exchangeBaseUrl,
);
- checkBackupInvariant(!!exchange);
+ checkBackupInvariant(!!exchangeDetails);
let wireFee: AmountJson | undefined;
- const feesForType = exchange.wireInfo?.feesForType;
+ const feesForType = exchangeDetails.wireInfo.feesForType;
checkBackupInvariant(!!feesForType);
for (const fee of feesForType[contractData.wireMethod] || []) {
if (
@@ -218,6 +219,7 @@ export async function importBackup(
[
Stores.config,
Stores.exchanges,
+ Stores.exchangeDetails,
Stores.coins,
Stores.denominations,
Stores.purchases,
@@ -245,21 +247,46 @@ export async function importBackup(
const tombstoneSet = new Set(backupBlob.tombstones);
+ // FIXME: Validate that the "details pointer" is correct
+
for (const backupExchange of backupBlob.exchanges) {
const existingExchange = await tx.get(
Stores.exchanges,
backupExchange.base_url,
);
+ if (existingExchange) {
+ continue;
+ }
+ await tx.put(Stores.exchanges, {
+ baseUrl: backupExchange.base_url,
+ detailsPointer: {
+ currency: backupExchange.currency,
+ masterPublicKey: backupExchange.master_public_key,
+ updateClock: backupExchange.update_clock,
+ },
+ permanent: true,
+ retryInfo: initRetryInfo(false),
+ updateStarted: { t_ms: "never" },
+ updateStatus: ExchangeUpdateStatus.Finished,
+ });
+ }
+
+ for (const backupExchangeDetails of backupBlob.exchange_details) {
+ const existingExchangeDetails = await tx.get(Stores.exchangeDetails, [
+ backupExchangeDetails.base_url,
+ backupExchangeDetails.currency,
+ backupExchangeDetails.master_public_key,
+ ]);
- if (!existingExchange) {
- const wireInfo: ExchangeWireInfo = {
- accounts: backupExchange.accounts.map((x) => ({
+ if (!existingExchangeDetails) {
+ const wireInfo: WireInfo = {
+ accounts: backupExchangeDetails.accounts.map((x) => ({
master_sig: x.master_sig,
payto_uri: x.payto_uri,
})),
feesForType: {},
};
- for (const fee of backupExchange.wire_fees) {
+ for (const fee of backupExchangeDetails.wire_fees) {
const w = (wireInfo.feesForType[fee.wire_type] ??= []);
w.push({
closingFee: Amounts.parseOrThrow(fee.closing_fee),
@@ -269,48 +296,39 @@ export async function importBackup(
wireFee: Amounts.parseOrThrow(fee.wire_fee),
});
}
- await tx.put(Stores.exchanges, {
- addComplete: true,
- baseUrl: backupExchange.base_url,
- builtIn: false,
- updateReason: undefined,
- permanent: true,
- retryInfo: initRetryInfo(),
- termsOfServiceAcceptedEtag: backupExchange.tos_etag_accepted,
+ await tx.put(Stores.exchangeDetails, {
+ exchangeBaseUrl: backupExchangeDetails.base_url,
+ termsOfServiceAcceptedEtag: backupExchangeDetails.tos_etag_accepted,
termsOfServiceText: undefined,
- termsOfServiceLastEtag: backupExchange.tos_etag_last,
- updateStarted: getTimestampNow(),
- updateStatus: ExchangeUpdateStatus.FetchKeys,
+ termsOfServiceLastEtag: backupExchangeDetails.tos_etag_last,
wireInfo,
- details: {
- currency: backupExchange.currency,
- reserveClosingDelay: backupExchange.reserve_closing_delay,
- auditors: backupExchange.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- lastUpdateTime: { t_ms: "never" },
- masterPublicKey: backupExchange.master_public_key,
- nextUpdateTime: { t_ms: "never" },
- protocolVersion: backupExchange.protocol_version,
- signingKeys: backupExchange.signing_keys.map((x) => ({
- key: x.key,
- master_sig: x.master_sig,
- stamp_end: x.stamp_end,
- stamp_expire: x.stamp_expire,
- stamp_start: x.stamp_start,
- })),
- },
+ currency: backupExchangeDetails.currency,
+ auditors: backupExchangeDetails.auditors.map((x) => ({
+ auditor_pub: x.auditor_pub,
+ auditor_url: x.auditor_url,
+ denomination_keys: x.denomination_keys,
+ })),
+ lastUpdateTime: { t_ms: "never" },
+ masterPublicKey: backupExchangeDetails.master_public_key,
+ nextUpdateTime: { t_ms: "never" },
+ protocolVersion: backupExchangeDetails.protocol_version,
+ reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
+ signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
+ key: x.key,
+ master_sig: x.master_sig,
+ stamp_end: x.stamp_end,
+ stamp_expire: x.stamp_expire,
+ stamp_start: x.stamp_start,
+ })),
});
}
- for (const backupDenomination of backupExchange.denominations) {
+ for (const backupDenomination of backupExchangeDetails.denominations) {
const denomPubHash =
cryptoComp.denomPubToHash[backupDenomination.denom_pub];
checkLogicInvariant(!!denomPubHash);
const existingDenom = await tx.get(Stores.denominations, [
- backupExchange.base_url,
+ backupExchangeDetails.base_url,
denomPubHash,
]);
if (!existingDenom) {
@@ -321,7 +339,7 @@ export async function importBackup(
await tx.put(Stores.denominations, {
denomPub: backupDenomination.denom_pub,
denomPubHash: denomPubHash,
- exchangeBaseUrl: backupExchange.base_url,
+ exchangeBaseUrl: backupExchangeDetails.base_url,
feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
@@ -378,7 +396,7 @@ export async function importBackup(
denomSig: backupCoin.denom_sig,
coinPub: compCoin.coinPub,
suspended: false,
- exchangeBaseUrl: backupExchange.base_url,
+ exchangeBaseUrl: backupExchangeDetails.base_url,
denomPub: backupDenomination.denom_pub,
denomPubHash,
status: backupCoin.fresh
@@ -390,7 +408,7 @@ export async function importBackup(
}
}
- for (const backupReserve of backupExchange.reserves) {
+ for (const backupReserve of backupExchangeDetails.reserves) {
const reservePub =
cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
@@ -414,7 +432,7 @@ export async function importBackup(
await tx.put(Stores.reserves, {
currency: instructedAmount.currency,
instructedAmount,
- exchangeBaseUrl: backupExchange.base_url,
+ exchangeBaseUrl: backupExchangeDetails.base_url,
reservePub,
reservePriv: backupReserve.reserve_priv,
requestedQuery: false,
@@ -436,7 +454,7 @@ export async function importBackup(
reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
initialDenomSel: await getDenomSelStateFromBackup(
tx,
- backupExchange.base_url,
+ backupExchangeDetails.base_url,
backupReserve.initial_selected_denoms,
),
});
@@ -457,10 +475,10 @@ export async function importBackup(
await tx.put(Stores.withdrawalGroups, {
denomsSel: await getDenomSelStateFromBackup(
tx,
- backupExchange.base_url,
+ backupExchangeDetails.base_url,
backupWg.selected_denoms,
),
- exchangeBaseUrl: backupExchange.base_url,
+ exchangeBaseUrl: backupExchangeDetails.base_url,
lastError: undefined,
rawWithdrawalAmount: Amounts.parseOrThrow(
backupWg.raw_withdrawal_amount,
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index 110e76596..2314c730d 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -155,8 +155,8 @@ async function computeBackupCryptoData(
proposalNoncePrivToPub: {},
reservePrivToPub: {},
};
- for (const backupExchange of backupContent.exchanges) {
- for (const backupDenom of backupExchange.denominations) {
+ for (const backupExchangeDetails of backupContent.exchange_details) {
+ for (const backupDenom of backupExchangeDetails.denominations) {
for (const backupCoin of backupDenom.coins) {
const coinPub = encodeCrock(
eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
@@ -175,7 +175,7 @@ async function computeBackupCryptoData(
hash(decodeCrock(backupDenom.denom_pub)),
);
}
- for (const backupReserve of backupExchange.reserves) {
+ for (const backupReserve of backupExchangeDetails.reserves) {
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
);
diff --git a/packages/taler-wallet-core/src/operations/currencies.ts b/packages/taler-wallet-core/src/operations/currencies.ts
index 1af30dfb5..5371d4a54 100644
--- a/packages/taler-wallet-core/src/operations/currencies.ts
+++ b/packages/taler-wallet-core/src/operations/currencies.ts
@@ -19,6 +19,7 @@
*/
import { ExchangeRecord, Stores } from "../db.js";
import { Logger } from "../index.js";
+import { getExchangeDetails } from "./exchanges.js";
import { InternalWalletState } from "./state.js";
const logger = new Logger("currencies.ts");
@@ -37,7 +38,12 @@ export async function getExchangeTrust(
): Promise<TrustInfo> {
let isTrusted = false;
let isAudited = false;
- const exchangeDetails = exchangeInfo.details;
+ const exchangeDetails = await ws.db.runWithReadTransaction(
+ [Stores.exchangeDetails, Stores.exchanges],
+ async (tx) => {
+ return getExchangeDetails(tx, exchangeInfo.baseUrl);
+ },
+ );
if (!exchangeDetails) {
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 4c87f122f..59c27b9cc 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -58,6 +58,7 @@ import { InternalWalletState } from "./state";
import { Logger } from "../util/logging.js";
import { DepositGroupRecord, Stores } from "../db.js";
import { guardOperationException } from "./errors.js";
+import { getExchangeDetails } from "./exchanges.js";
/**
* Logger.
@@ -308,14 +309,17 @@ export async function createDepositGroup(
const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
const exchangeInfos: { url: string; master_pub: string }[] = [];
for (const e of allExchanges) {
- if (!e.details) {
- continue;
- }
- if (e.details.currency != amount.currency) {
+ const details = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ return getExchangeDetails(tx, e.baseUrl);
+ },
+ );
+ if (!details) {
continue;
}
exchangeInfos.push({
- master_pub: e.details.masterPublicKey,
+ master_pub: details.masterPublicKey,
url: e.baseUrl,
});
}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index e8833699d..be9a383d2 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -19,18 +19,23 @@
*/
import {
Amounts,
+ Auditor,
codecForExchangeKeysJson,
codecForExchangeWireJson,
compare,
Denomination,
Duration,
durationFromSpec,
+ ExchangeSignKeyJson,
+ ExchangeWireJson,
getTimestampNow,
isTimestampExpired,
NotificationType,
parsePaytoUri,
+ Recoup,
TalerErrorCode,
TalerErrorDetails,
+ Timestamp,
} from "@gnu-taler/taler-util";
import {
DenominationRecord,
@@ -40,6 +45,8 @@ import {
ExchangeUpdateStatus,
WireFee,
ExchangeUpdateReason,
+ ExchangeDetailsRecord,
+ WireInfo,
} from "../db.js";
import {
Logger,
@@ -47,14 +54,16 @@ import {
readSuccessResponseJsonOrThrow,
getExpiryTimestamp,
readSuccessResponseTextOrThrow,
+ encodeCrock,
+ hash,
+ decodeCrock,
} from "../index.js";
import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-import { checkDbInvariant } from "../util/invariants.js";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js";
import {
makeErrorDetails,
- OperationFailedAndReportedError,
guardOperationException,
+ OperationFailedError,
} from "./errors.js";
import { createRecoupGroup, processRecoupGroup } from "./recoup.js";
import { InternalWalletState } from "./state.js";
@@ -62,15 +71,17 @@ import {
WALLET_CACHE_BREAKER_CLIENT_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
} from "./versions.js";
+import { HttpRequestLibrary } from "../util/http.js";
+import { CryptoApi } from "../crypto/workers/cryptoApi.js";
+import { TransactionHandle } from "../util/query.js";
const logger = new Logger("exchanges.ts");
-async function denominationRecordFromKeys(
- ws: InternalWalletState,
+function denominationRecordFromKeys(
exchangeBaseUrl: string,
denomIn: Denomination,
-): Promise<DenominationRecord> {
- const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
+): DenominationRecord {
+ const denomPubHash = encodeCrock(hash(decodeCrock(denomIn.denom_pub)));
const d: DenominationRecord = {
denomPub: denomIn.denom_pub,
denomPubHash,
@@ -115,29 +126,206 @@ function getExchangeRequestTimeout(e: ExchangeRecord): Duration {
return { d_ms: 5000 };
}
+interface ExchangeTosDownloadResult {
+ tosText: string;
+ tosEtag: string;
+}
+
+async function downloadExchangeWithTermsOfService(
+ exchangeBaseUrl: string,
+ http: HttpRequestLibrary,
+ timeout: Duration,
+): Promise<ExchangeTosDownloadResult> {
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+ reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+ const headers = {
+ Accept: "text/plain",
+ };
+
+ const resp = await http.get(reqUrl.href, {
+ headers,
+ timeout,
+ });
+ const tosText = await readSuccessResponseTextOrThrow(resp);
+ const tosEtag = resp.headers.get("etag") || "unknown";
+
+ return { tosText, tosEtag };
+}
+
+export async function getExchangeDetails(
+ tx: TransactionHandle<
+ typeof Stores.exchanges | typeof Stores.exchangeDetails
+ >,
+ exchangeBaseUrl: string,
+): Promise<ExchangeDetailsRecord | undefined> {
+ const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!r) {
+ return;
+ }
+ const dp = r.detailsPointer;
+ if (!dp) {
+ return;
+ }
+ const { currency, masterPublicKey } = dp;
+ return await tx.get(Stores.exchangeDetails, [
+ r.baseUrl,
+ currency,
+ masterPublicKey,
+ ]);
+}
+
+export async function acceptExchangeTermsOfService(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ etag: string | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ const d = await getExchangeDetails(tx, exchangeBaseUrl);
+ if (d) {
+ d.termsOfServiceAcceptedEtag = etag;
+ await tx.put(Stores.exchangeDetails, d);
+ }
+ },
+ );
+}
+
+async function validateWireInfo(
+ wireInfo: ExchangeWireJson,
+ masterPublicKey: string,
+ cryptoApi: CryptoApi,
+): Promise<WireInfo> {
+ for (const a of wireInfo.accounts) {
+ logger.trace("validating exchange acct");
+ const isValid = await cryptoApi.isValidWireAccount(
+ a.payto_uri,
+ a.master_sig,
+ masterPublicKey,
+ );
+ if (!isValid) {
+ throw Error("exchange acct signature invalid");
+ }
+ }
+ const feesForType: { [wireMethod: string]: WireFee[] } = {};
+ for (const wireMethod of Object.keys(wireInfo.fees)) {
+ const feeList: WireFee[] = [];
+ for (const x of wireInfo.fees[wireMethod]) {
+ const startStamp = x.start_date;
+ const endStamp = x.end_date;
+ const fee: WireFee = {
+ closingFee: Amounts.parseOrThrow(x.closing_fee),
+ endStamp,
+ sig: x.sig,
+ startStamp,
+ wireFee: Amounts.parseOrThrow(x.wire_fee),
+ };
+ const isValid = await cryptoApi.isValidWireFee(
+ wireMethod,
+ fee,
+ masterPublicKey,
+ );
+ if (!isValid) {
+ throw Error("exchange wire fee signature invalid");
+ }
+ feeList.push(fee);
+ }
+ feesForType[wireMethod] = feeList;
+ }
+
+ return {
+ accounts: wireInfo.accounts,
+ feesForType,
+ };
+}
+
/**
- * Fetch the exchange's /keys and update our database accordingly.
+ * Fetch wire information for an exchange.
*
- * Exceptions thrown in this method must be caught and reported
- * in the pending operations.
+ * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
*/
-async function updateExchangeWithKeys(
+async function downloadExchangeWithWireInfo(
+ exchangeBaseUrl: string,
+ http: HttpRequestLibrary,
+ timeout: Duration,
+): Promise<ExchangeWireJson> {
+ const reqUrl = new URL("wire", exchangeBaseUrl);
+ reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+
+ const resp = await http.get(reqUrl.href, {
+ timeout,
+ });
+ const wireInfo = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeWireJson(),
+ );
+
+ return wireInfo;
+}
+
+export async function updateExchangeFromUrl(
ws: InternalWalletState,
baseUrl: string,
-): Promise<void> {
- const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
+ forceNow = false,
+): Promise<{
+ exchange: ExchangeRecord;
+ exchangeDetails: ExchangeDetailsRecord;
+}> {
+ const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+ handleExchangeUpdateError(ws, baseUrl, e);
+ return await guardOperationException(
+ () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
+ onOpErr,
+ );
+}
- if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
- return;
+async function provideExchangeRecord(
+ ws: InternalWalletState,
+ baseUrl: string,
+ now: Timestamp,
+): Promise<ExchangeRecord> {
+ let r = await ws.db.get(Stores.exchanges, baseUrl);
+ if (!r) {
+ const newExchangeRecord: ExchangeRecord = {
+ permanent: true,
+ baseUrl: baseUrl,
+ updateStatus: ExchangeUpdateStatus.FetchKeys,
+ updateStarted: now,
+ updateReason: ExchangeUpdateReason.Initial,
+ retryInfo: initRetryInfo(false),
+ detailsPointer: undefined,
+ };
+ await ws.db.put(Stores.exchanges, newExchangeRecord);
+ r = newExchangeRecord;
}
+ return r;
+}
- logger.info("updating exchange /keys info");
+interface ExchangeKeysDownloadResult {
+ masterPublicKey: string;
+ currency: string;
+ auditors: Auditor[];
+ currentDenominations: DenominationRecord[];
+ protocolVersion: string;
+ signingKeys: ExchangeSignKeyJson[];
+ reserveClosingDelay: Duration;
+ expiry: Timestamp;
+ recoup: Recoup[];
+}
+/**
+ * Download and validate an exchange's /keys data.
+ */
+async function downloadKeysInfo(
+ baseUrl: string,
+ http: HttpRequestLibrary,
+ timeout: Duration,
+): Promise<ExchangeKeysDownloadResult> {
const keysUrl = new URL("keys", baseUrl);
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
- const resp = await ws.http.get(keysUrl.href, {
- timeout: getExchangeRequestTimeout(existingExchangeRecord),
+ const resp = await http.get(keysUrl.href, {
+ timeout,
});
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
resp,
@@ -155,8 +343,7 @@ async function updateExchangeWithKeys(
exchangeBaseUrl: baseUrl,
},
);
- await handleExchangeUpdateError(ws, baseUrl, opErr);
- throw new OperationFailedAndReportedError(opErr);
+ throw new OperationFailedError(opErr);
}
const protocolVersion = exchangeKeysJson.version;
@@ -171,70 +358,138 @@ async function updateExchangeWithKeys(
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
},
);
- await handleExchangeUpdateError(ws, baseUrl, opErr);
- throw new OperationFailedAndReportedError(opErr);
+ throw new OperationFailedError(opErr);
}
- const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
- .currency;
-
- logger.trace("processing denominations");
+ const currency = Amounts.parseOrThrow(
+ exchangeKeysJson.denoms[0].value,
+ ).currency.toUpperCase();
- const newDenominations = await Promise.all(
- exchangeKeysJson.denoms.map((d) =>
- denominationRecordFromKeys(ws, baseUrl, d),
+ return {
+ masterPublicKey: exchangeKeysJson.master_public_key,
+ currency,
+ auditors: exchangeKeysJson.auditors,
+ currentDenominations: exchangeKeysJson.denoms.map((d) =>
+ denominationRecordFromKeys(baseUrl, d),
),
+ protocolVersion: exchangeKeysJson.version,
+ signingKeys: exchangeKeysJson.signkeys,
+ reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
+ expiry: getExpiryTimestamp(resp, {
+ minDuration: durationFromSpec({ hours: 1 }),
+ }),
+ recoup: exchangeKeysJson.recoup ?? [],
+ };
+}
+
+/**
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+async function updateExchangeFromUrlImpl(
+ ws: InternalWalletState,
+ baseUrl: string,
+ forceNow = false,
+): Promise<{
+ exchange: ExchangeRecord;
+ exchangeDetails: ExchangeDetailsRecord;
+}> {
+ logger.trace(`updating exchange info for ${baseUrl}`);
+ const now = getTimestampNow();
+ baseUrl = canonicalizeBaseUrl(baseUrl);
+
+ const r = await provideExchangeRecord(ws, baseUrl, now);
+
+ logger.info("updating exchange /keys info");
+
+ const timeout = getExchangeRequestTimeout(r);
+
+ const keysInfo = await downloadKeysInfo(baseUrl, ws.http, timeout);
+
+ const wireInfoDownload = await downloadExchangeWithWireInfo(
+ baseUrl,
+ ws.http,
+ timeout,
);
- logger.trace("done with processing denominations");
+ const wireInfo = await validateWireInfo(
+ wireInfoDownload,
+ keysInfo.masterPublicKey,
+ ws.cryptoApi,
+ );
- const lastUpdateTimestamp = getTimestampNow();
+ const tosDownload = await downloadExchangeWithTermsOfService(
+ baseUrl,
+ ws.http,
+ timeout,
+ );
- const recoupGroupId: string | undefined = undefined;
+ let recoupGroupId: string | undefined = undefined;
- await ws.db.runWithWriteTransaction(
- [Stores.exchanges, Stores.denominations, Stores.recoupGroups, Stores.coins],
+ const updated = await ws.db.runWithWriteTransaction(
+ [
+ Stores.exchanges,
+ Stores.exchangeDetails,
+ Stores.denominations,
+ Stores.recoupGroups,
+ Stores.coins,
+ ],
async (tx) => {
const r = await tx.get(Stores.exchanges, baseUrl);
if (!r) {
logger.warn(`exchange ${baseUrl} no longer present`);
return;
}
- if (r.details) {
+ let details = await getExchangeDetails(tx, r.baseUrl);
+ if (details) {
// FIXME: We need to do some consistency checks!
}
// FIXME: validate signing keys and merge with old set
- r.details = {
- auditors: exchangeKeysJson.auditors,
- currency: currency,
- lastUpdateTime: lastUpdateTimestamp,
- masterPublicKey: exchangeKeysJson.master_public_key,
- protocolVersion: protocolVersion,
- signingKeys: exchangeKeysJson.signkeys,
- nextUpdateTime: getExpiryTimestamp(resp, {
- minDuration: durationFromSpec({ hours: 1 }),
- }),
- reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
+ details = {
+ auditors: keysInfo.auditors,
+ currency: keysInfo.currency,
+ lastUpdateTime: now,
+ masterPublicKey: keysInfo.masterPublicKey,
+ protocolVersion: keysInfo.protocolVersion,
+ signingKeys: keysInfo.signingKeys,
+ nextUpdateTime: keysInfo.expiry,
+ reserveClosingDelay: keysInfo.reserveClosingDelay,
+ exchangeBaseUrl: r.baseUrl,
+ wireInfo,
+ termsOfServiceText: tosDownload.tosText,
+ termsOfServiceAcceptedEtag: undefined,
+ termsOfServiceLastEtag: tosDownload.tosEtag,
};
r.updateStatus = ExchangeUpdateStatus.FetchWire;
+ // FIXME: only update if pointer got updated
r.lastError = undefined;
r.retryInfo = initRetryInfo(false);
+ // New denominations might be available.
+ r.nextRefreshCheck = undefined;
+ r.detailsPointer = {
+ currency: details.currency,
+ masterPublicKey: details.masterPublicKey,
+ // FIXME: only change if pointer really changed
+ updateClock: getTimestampNow(),
+ };
await tx.put(Stores.exchanges, r);
+ await tx.put(Stores.exchangeDetails, details);
- for (const newDenom of newDenominations) {
+ for (const currentDenom of keysInfo.currentDenominations) {
const oldDenom = await tx.get(Stores.denominations, [
baseUrl,
- newDenom.denomPubHash,
+ currentDenom.denomPubHash,
]);
if (oldDenom) {
// FIXME: Do consistency check
} else {
- await tx.put(Stores.denominations, newDenom);
+ await tx.put(Stores.denominations, currentDenom);
}
}
// Handle recoup
- const recoupDenomList = exchangeKeysJson.recoup ?? [];
+ const recoupDenomList = keysInfo.recoup;
const newlyRevokedCoinPubs: string[] = [];
logger.trace("recoup list from exchange", recoupDenomList);
for (const recoupInfo of recoupDenomList) {
@@ -264,8 +519,12 @@ async function updateExchangeWithKeys(
}
if (newlyRevokedCoinPubs.length != 0) {
logger.trace("recouping coins", newlyRevokedCoinPubs);
- await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
+ recoupGroupId = await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
}
+ return {
+ exchange: r,
+ exchangeDetails: details,
+ };
},
);
@@ -277,257 +536,16 @@ async function updateExchangeWithKeys(
});
}
- logger.trace("done updating exchange /keys");
-}
-
-async function updateExchangeFinalize(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<void> {
- const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
- return;
+ if (!updated) {
+ throw Error("something went wrong with updating the exchange");
}
- await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- if (r.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
- return;
- }
- r.addComplete = true;
- r.updateStatus = ExchangeUpdateStatus.Finished;
- // Reset time to next auto refresh check,
- // as now new denominations might be available.
- r.nextRefreshCheck = undefined;
- await tx.put(Stores.exchanges, r);
- });
-}
-async function updateExchangeWithTermsOfService(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<void> {
- const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
- return;
- }
- const reqUrl = new URL("terms", exchangeBaseUrl);
- reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
- const headers = {
- Accept: "text/plain",
+ return {
+ exchange: updated.exchange,
+ exchangeDetails: updated.exchangeDetails,
};
-
- const resp = await ws.http.get(reqUrl.href, {
- headers,
- timeout: getExchangeRequestTimeout(exchange),
- });
- const tosText = await readSuccessResponseTextOrThrow(resp);
- const tosEtag = resp.headers.get("etag") || undefined;
-
- await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
- return;
- }
- r.termsOfServiceText = tosText;
- r.termsOfServiceLastEtag = tosEtag;
- r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
- await tx.put(Stores.exchanges, r);
- });
-}
-
-export async function acceptExchangeTermsOfService(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- etag: string | undefined,
-): Promise<void> {
- await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- r.termsOfServiceAcceptedEtag = etag;
- await tx.put(Stores.exchanges, r);
- });
-}
-
-/**
- * Fetch wire information for an exchange and store it in the database.
- *
- * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
- */
-async function updateExchangeWithWireInfo(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<void> {
- const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
- return;
- }
- const details = exchange.details;
- if (!details) {
- throw Error("invalid exchange state");
- }
- const reqUrl = new URL("wire", exchangeBaseUrl);
- reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-
- const resp = await ws.http.get(reqUrl.href, {
- timeout: getExchangeRequestTimeout(exchange),
- });
- const wireInfo = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeWireJson(),
- );
-
- for (const a of wireInfo.accounts) {
- logger.trace("validating exchange acct");
- const isValid = await ws.cryptoApi.isValidWireAccount(
- a.payto_uri,
- a.master_sig,
- details.masterPublicKey,
- );
- if (!isValid) {
- throw Error("exchange acct signature invalid");
- }
- }
- const feesForType: { [wireMethod: string]: WireFee[] } = {};
- for (const wireMethod of Object.keys(wireInfo.fees)) {
- const feeList: WireFee[] = [];
- for (const x of wireInfo.fees[wireMethod]) {
- const startStamp = x.start_date;
- const endStamp = x.end_date;
- const fee: WireFee = {
- closingFee: Amounts.parseOrThrow(x.closing_fee),
- endStamp,
- sig: x.sig,
- startStamp,
- wireFee: Amounts.parseOrThrow(x.wire_fee),
- };
- const isValid = await ws.cryptoApi.isValidWireFee(
- wireMethod,
- fee,
- details.masterPublicKey,
- );
- if (!isValid) {
- throw Error("exchange wire fee signature invalid");
- }
- feeList.push(fee);
- }
- feesForType[wireMethod] = feeList;
- }
-
- await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
- return;
- }
- r.wireInfo = {
- accounts: wireInfo.accounts,
- feesForType: feesForType,
- };
- r.updateStatus = ExchangeUpdateStatus.FetchTerms;
- r.lastError = undefined;
- r.retryInfo = initRetryInfo(false);
- await tx.put(Stores.exchanges, r);
- });
}
-export async function updateExchangeFromUrl(
- ws: InternalWalletState,
- baseUrl: string,
- forceNow = false,
-): Promise<ExchangeRecord> {
- const onOpErr = (e: TalerErrorDetails): Promise<void> =>
- handleExchangeUpdateError(ws, baseUrl, e);
- return await guardOperationException(
- () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
- onOpErr,
- );
-}
-
-/**
- * Update or add exchange DB entry by fetching the /keys and /wire information.
- * Optionally link the reserve entry to the new or existing
- * exchange entry in then DB.
- */
-async function updateExchangeFromUrlImpl(
- ws: InternalWalletState,
- baseUrl: string,
- forceNow = false,
-): Promise<ExchangeRecord> {
- logger.trace(`updating exchange info for ${baseUrl}`);
- const now = getTimestampNow();
- baseUrl = canonicalizeBaseUrl(baseUrl);
-
- let r = await ws.db.get(Stores.exchanges, baseUrl);
- if (!r) {
- const newExchangeRecord: ExchangeRecord = {
- builtIn: false,
- addComplete: false,
- permanent: true,
- baseUrl: baseUrl,
- details: undefined,
- wireInfo: undefined,
- updateStatus: ExchangeUpdateStatus.FetchKeys,
- updateStarted: now,
- updateReason: ExchangeUpdateReason.Initial,
- termsOfServiceAcceptedEtag: undefined,
- termsOfServiceLastEtag: undefined,
- termsOfServiceText: undefined,
- retryInfo: initRetryInfo(false),
- };
- await ws.db.put(Stores.exchanges, newExchangeRecord);
- } else {
- await ws.db.runWithWriteTransaction([Stores.exchanges], async (t) => {
- const rec = await t.get(Stores.exchanges, baseUrl);
- if (!rec) {
- return;
- }
- if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys) {
- const t = rec.details?.nextUpdateTime;
- if (!forceNow && t && !isTimestampExpired(t)) {
- return;
- }
- }
- if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
- rec.updateReason = ExchangeUpdateReason.Forced;
- }
- rec.updateStarted = now;
- rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
- rec.lastError = undefined;
- rec.retryInfo = initRetryInfo(false);
- t.put(Stores.exchanges, rec);
- });
- }
-
- await updateExchangeWithKeys(ws, baseUrl);
- await updateExchangeWithWireInfo(ws, baseUrl);
- await updateExchangeWithTermsOfService(ws, baseUrl);
- await updateExchangeFinalize(ws, baseUrl);
-
- const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
- checkDbInvariant(!!updatedExchange);
- return updatedExchange;
-}
-
-
export async function getExchangePaytoUri(
ws: InternalWalletState,
exchangeBaseUrl: string,
@@ -535,15 +553,14 @@ export async function getExchangePaytoUri(
): Promise<string> {
// We do the update here, since the exchange might not even exist
// yet in our database.
- const exchangeRecord = await updateExchangeFromUrl(ws, exchangeBaseUrl);
- if (!exchangeRecord) {
- throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
- }
- const exchangeWireInfo = exchangeRecord.wireInfo;
- if (!exchangeWireInfo) {
- throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
- }
- for (const account of exchangeWireInfo.accounts) {
+ const details = await ws.db.runWithReadTransaction(
+ [Stores.exchangeDetails, Stores.exchanges],
+ async (tx) => {
+ return getExchangeDetails(tx, exchangeBaseUrl);
+ },
+ );
+ const accounts = details?.wireInfo.accounts ?? [];
+ for (const account of accounts) {
const res = parsePaytoUri(account.payto_uri);
if (!res) {
continue;
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 1ed8d72b9..dad460b8c 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -94,6 +94,7 @@ import {
import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js";
import { ContractTermsUtil } from "../util/contractTerms.js";
+import { getExchangeDetails } from "./exchanges.js";
/**
* Logger.
@@ -170,11 +171,16 @@ export async function getEffectiveDepositAmount(
exchangeSet.add(coin.exchangeBaseUrl);
}
for (const exchangeUrl of exchangeSet.values()) {
- const exchange = await ws.db.get(Stores.exchanges, exchangeUrl);
- if (!exchange?.wireInfo) {
+ const exchangeDetails = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ return getExchangeDetails(tx, exchangeUrl);
+ },
+ );
+ if (!exchangeDetails) {
continue;
}
- const fee = exchange.wireInfo.feesForType[wireType].find((x) => {
+ const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
return timestampIsBetween(getTimestampNow(), x.startStamp, x.endStamp);
})?.wireFee;
if (fee) {
@@ -240,11 +246,16 @@ export async function getCandidatePayCoins(
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
for (const exchange of exchanges) {
let isOkay = false;
- const exchangeDetails = exchange.details;
+ const exchangeDetails = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ return getExchangeDetails(tx, exchange.baseUrl);
+ },
+ );
if (!exchangeDetails) {
continue;
}
- const exchangeFees = exchange.wireInfo;
+ const exchangeFees = exchangeDetails.wireInfo;
if (!exchangeFees) {
continue;
}
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 01920a85b..85f8faa18 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -37,9 +37,10 @@ import {
getDurationRemaining,
durationMin,
} from "@gnu-taler/taler-util";
-import { Store, TransactionHandle } from "../util/query";
+import { TransactionHandle } from "../util/query";
import { InternalWalletState } from "./state";
import { getBalancesInsideTransaction } from "./balance";
+import { getExchangeDetails } from "./exchanges.js";
function updateRetryDelay(
oldDelay: Duration,
@@ -52,12 +53,14 @@ function updateRetryDelay(
}
async function gatherExchangePending(
- tx: TransactionHandle<typeof Stores.exchanges>,
+ tx: TransactionHandle<
+ typeof Stores.exchanges | typeof Stores.exchangeDetails
+ >,
now: Timestamp,
resp: PendingOperationsResponse,
onlyDue = false,
): Promise<void> {
- await tx.iter(Stores.exchanges).forEach((e) => {
+ await tx.iter(Stores.exchanges).forEachAsync(async (e) => {
switch (e.updateStatus) {
case ExchangeUpdateStatus.Finished:
if (e.lastError) {
@@ -71,30 +74,9 @@ async function gatherExchangePending(
},
});
}
- if (!e.details) {
- resp.pendingOperations.push({
- type: PendingOperationType.Bug,
- givesLifeness: false,
- message:
- "Exchange record does not have details, but no update finished.",
- details: {
- exchangeBaseUrl: e.baseUrl,
- },
- });
- }
- if (!e.wireInfo) {
- resp.pendingOperations.push({
- type: PendingOperationType.Bug,
- givesLifeness: false,
- message:
- "Exchange record does not have wire info, but no update finished.",
- details: {
- exchangeBaseUrl: e.baseUrl,
- },
- });
- }
+ const details = await getExchangeDetails(tx, e.baseUrl);
const keysUpdateRequired =
- e.details && e.details.nextUpdateTime.t_ms < now.t_ms;
+ details && details.nextUpdateTime.t_ms < now.t_ms;
if (keysUpdateRequired) {
resp.pendingOperations.push({
type: PendingOperationType.ExchangeUpdate,
@@ -106,7 +88,7 @@ async function gatherExchangePending(
});
}
if (
- e.details &&
+ details &&
(!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms)
) {
resp.pendingOperations.push({
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index 337892f77..aa551e8da 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -24,9 +24,25 @@
/**
* Imports.
*/
-import { Amounts, codecForRecoupConfirmation, getTimestampNow, NotificationType, RefreshReason, TalerErrorDetails } from "@gnu-taler/taler-util";
+import {
+ Amounts,
+ codecForRecoupConfirmation,
+ getTimestampNow,
+ NotificationType,
+ RefreshReason,
+ TalerErrorDetails,
+} from "@gnu-taler/taler-util";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
-import { CoinRecord, CoinSourceType, CoinStatus, RecoupGroupRecord, RefreshCoinSource, ReserveRecordStatus, Stores, WithdrawCoinSource } from "../db.js";
+import {
+ CoinRecord,
+ CoinSourceType,
+ CoinStatus,
+ RecoupGroupRecord,
+ RefreshCoinSource,
+ ReserveRecordStatus,
+ Stores,
+ WithdrawCoinSource,
+} from "../db.js";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { Logger } from "../util/logging";
@@ -34,6 +50,7 @@ import { TransactionHandle } from "../util/query";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import { URL } from "../util/url";
import { guardOperationException } from "./errors";
+import { getExchangeDetails } from "./exchanges.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh";
import { getReserveRequestTimeout, processReserve } from "./reserves";
import { InternalWalletState } from "./state";
@@ -155,12 +172,13 @@ async function recoupWithdrawCoin(
throw Error(`Coin's reserve doesn't match reserve on recoup`);
}
- const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
- if (!exchange) {
- // FIXME: report inconsistency?
- return;
- }
- const exchangeDetails = exchange.details;
+ const exchangeDetails = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ return getExchangeDetails(tx, reserve.exchangeBaseUrl);
+ },
+ );
+
if (!exchangeDetails) {
// FIXME: report inconsistency?
return;
@@ -232,13 +250,14 @@ async function recoupRefreshCoin(
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
}
- const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
- if (!exchange) {
- logger.warn("exchange for recoup does not exist anymore");
- // FIXME: report inconsistency?
- return;
- }
- const exchangeDetails = exchange.details;
+ const exchangeDetails = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ // FIXME: Get the exchange details based on the
+ // exchange master public key instead of via just the URL.
+ return getExchangeDetails(tx, coin.exchangeBaseUrl);
+ },
+ );
if (!exchangeDetails) {
// FIXME: report inconsistency?
logger.warn("exchange details for recoup not found");
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 84460fb88..9d4390abd 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -122,7 +122,7 @@ async function refreshCreateSession(
throw Error("Can't refresh, coin not found");
}
- const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
+ const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
if (!exchange) {
throw Error("db inconsistent: exchange of coin not found");
}
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
index d8821d560..d06ce31ed 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -58,7 +58,11 @@ import {
updateRetryInfoTimeout,
} from "../util/retries.js";
import { guardOperationException, OperationFailedError } from "./errors.js";
-import { updateExchangeFromUrl, getExchangePaytoUri } from "./exchanges.js";
+import {
+ updateExchangeFromUrl,
+ getExchangePaytoUri,
+ getExchangeDetails,
+} from "./exchanges.js";
import { InternalWalletState } from "./state.js";
import {
updateWithdrawalDenoms,
@@ -148,12 +152,15 @@ export async function createReserve(
};
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
- const exchangeDetails = exchangeInfo.details;
+ const exchangeDetails = exchangeInfo.exchangeDetails;
if (!exchangeDetails) {
logger.trace(exchangeDetails);
throw Error("exchange not updated");
}
- const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
+ const { isAudited, isTrusted } = await getExchangeTrust(
+ ws,
+ exchangeInfo.exchange,
+ );
const resp = await ws.db.runWithWriteTransaction(
[Stores.exchangeTrustStore, Stores.reserves, Stores.bankWithdrawUris],
@@ -728,7 +735,11 @@ export async function createTalerWithdrawReserve(
* Get payto URIs needed to fund a reserve.
*/
export async function getFundingPaytoUris(
- tx: TransactionHandle<typeof Stores.reserves | typeof Stores.exchanges>,
+ tx: TransactionHandle<
+ | typeof Stores.reserves
+ | typeof Stores.exchanges
+ | typeof Stores.exchangeDetails
+ >,
reservePub: string,
): Promise<string[]> {
const r = await tx.get(Stores.reserves, reservePub);
@@ -736,13 +747,13 @@ export async function getFundingPaytoUris(
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
return [];
}
- const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
- if (!exchange) {
+ const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
+ if (!exchangeDetails) {
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
return [];
}
const plainPaytoUris =
- exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+ exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
if (!plainPaytoUris) {
logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
return [];
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 1df7c7be2..42ed2d2ec 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -38,6 +38,7 @@ import {
OrderShortInfo,
} from "@gnu-taler/taler-util";
import { getFundingPaytoUris } from "./reserves";
+import { getExchangeDetails } from "./exchanges.js";
/**
* Create an event ID from the type and the primary key for the event.
@@ -89,6 +90,7 @@ export async function getTransactions(
Stores.coins,
Stores.denominations,
Stores.exchanges,
+ Stores.exchangeDetails,
Stores.proposals,
Stores.purchases,
Stores.refreshGroups,
@@ -134,15 +136,18 @@ export async function getTransactions(
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
- const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
- if (!exchange) {
+ const exchangeDetails = await getExchangeDetails(
+ tx,
+ wsr.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) {
// FIXME: report somehow
return;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris:
- exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
+ exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
};
}
transactions.push({
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 0ff69cb5a..5f050620a 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -35,7 +35,7 @@ import {
PlanchetRecord,
DenomSelectionState,
ExchangeRecord,
- ExchangeWireInfo,
+ ExchangeDetailsRecord,
} from "../db";
import {
BankWithdrawDetails,
@@ -51,7 +51,7 @@ import {
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "./state";
import { Logger } from "../util/logging";
-import { updateExchangeFromUrl } from "./exchanges";
+import { getExchangeDetails, updateExchangeFromUrl } from "./exchanges";
import {
WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -94,6 +94,8 @@ interface ExchangeWithdrawDetails {
*/
exchangeInfo: ExchangeRecord;
+ exchangeDetails: ExchangeDetailsRecord;
+
/**
* Filtered wire info to send to the bank.
*/
@@ -115,11 +117,6 @@ interface ExchangeWithdrawDetails {
overhead: AmountJson;
/**
- * Wire fees from the exchange.
- */
- wireFees: ExchangeWireInfo;
-
- /**
* Does the wallet know about an auditor for
* the exchange that the reserve.
*/
@@ -639,12 +636,12 @@ export async function updateWithdrawalDenoms(
ws: InternalWalletState,
exchangeBaseUrl: string,
): Promise<void> {
- const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- logger.error("exchange not found");
- throw Error(`exchange ${exchangeBaseUrl} not found`);
- }
- const exchangeDetails = exchange.details;
+ const exchangeDetails = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ return getExchangeDetails(tx, exchangeBaseUrl);
+ },
+ );
if (!exchangeDetails) {
logger.error("exchange details not available");
throw Error(`exchange ${exchangeBaseUrl} details not available`);
@@ -849,25 +846,19 @@ export async function getExchangeWithdrawalInfo(
baseUrl: string,
amount: AmountJson,
): Promise<ExchangeWithdrawDetails> {
- const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl);
- const exchangeDetails = exchangeInfo.details;
- if (!exchangeDetails) {
- throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
- }
- const exchangeWireInfo = exchangeInfo.wireInfo;
- if (!exchangeWireInfo) {
- throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
- }
-
+ const { exchange, exchangeDetails } = await updateExchangeFromUrl(
+ ws,
+ baseUrl,
+ );
await updateWithdrawalDenoms(ws, baseUrl);
const denoms = await getCandidateWithdrawalDenoms(ws, baseUrl);
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
const exchangeWireAccounts: string[] = [];
- for (const account of exchangeWireInfo.accounts) {
+ for (const account of exchangeDetails.wireInfo.accounts) {
exchangeWireAccounts.push(account.payto_uri);
}
- const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
+ const { isTrusted, isAudited } = await getExchangeTrust(ws, exchange);
let earliestDepositExpiration =
selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
@@ -904,10 +895,10 @@ export async function getExchangeWithdrawalInfo(
let tosAccepted = false;
- if (exchangeInfo.termsOfServiceLastEtag) {
+ if (exchangeDetails.termsOfServiceLastEtag) {
if (
- exchangeInfo.termsOfServiceAcceptedEtag ===
- exchangeInfo.termsOfServiceLastEtag
+ exchangeDetails.termsOfServiceAcceptedEtag ===
+ exchangeDetails.termsOfServiceLastEtag
) {
tosAccepted = true;
}
@@ -920,7 +911,8 @@ export async function getExchangeWithdrawalInfo(
const ret: ExchangeWithdrawDetails = {
earliestDepositExpiration,
- exchangeInfo,
+ exchangeInfo: exchange,
+ exchangeDetails,
exchangeWireAccounts,
exchangeVersion: exchangeDetails.protocolVersion || "unknown",
isAudited,
@@ -932,7 +924,6 @@ export async function getExchangeWithdrawalInfo(
trustedAuditorPubs: [],
versionMatch,
walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
- wireFees: exchangeWireInfo,
withdrawFee,
termsOfServiceAccepted: tosAccepted,
};
@@ -960,29 +951,25 @@ export async function getWithdrawalDetailsForUri(
}
}
- const exchangesRes: (ExchangeListItem | undefined)[] = await ws.db
- .iter(Stores.exchanges)
- .map((x) => {
- const details = x.details;
- if (!details) {
- return undefined;
- }
- if (!x.addComplete) {
- return undefined;
- }
- if (!x.wireInfo) {
- return undefined;
- }
- if (details.currency !== info.amount.currency) {
- return undefined;
- }
- return {
- exchangeBaseUrl: x.baseUrl,
+ const exchanges: ExchangeListItem[] = [];
+
+ const exchangeRecords = await ws.db.iter(Stores.exchanges).toArray();
+
+ for (const r of exchangeRecords) {
+ const details = await ws.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.exchangeDetails],
+ async (tx) => {
+ return getExchangeDetails(tx, r.baseUrl);
+ },
+ );
+ if (details) {
+ exchanges.push({
+ exchangeBaseUrl: details.exchangeBaseUrl,
currency: details.currency,
- paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
- };
- });
- const exchanges = exchangesRes.filter((x) => !!x) as ExchangeListItem[];
+ paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
+ });
+ }
+ }
return {
amount: Amounts.stringify(info.amount),
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 317d81ceb..d968fea47 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -105,6 +105,7 @@ import {
CoinRecord,
CoinSourceType,
DenominationRecord,
+ ExchangeDetailsRecord,
ExchangeRecord,
PurchaseRecord,
RefundState,
@@ -232,7 +233,7 @@ export class Wallet {
exchangeBaseUrl,
amount,
);
- const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
+ const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
(x) => x.payto_uri,
);
if (!paytoUris) {
@@ -586,13 +587,14 @@ export class Wallet {
/**
* Update or add exchange DB entry by fetching the /keys and /wire information.
- * Optionally link the reserve entry to the new or existing
- * exchange entry in then DB.
*/
async updateExchangeFromUrl(
baseUrl: string,
force = false,
- ): Promise<ExchangeRecord> {
+ ): Promise<{
+ exchange: ExchangeRecord;
+ exchangeDetails: ExchangeDetailsRecord;
+ }> {
try {
return updateExchangeFromUrl(this.ws, baseUrl, force);
} finally {
@@ -601,14 +603,16 @@ export class Wallet {
}
async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> {
- const exchange = await this.updateExchangeFromUrl(exchangeBaseUrl);
- const tos = exchange.termsOfServiceText;
- const currentEtag = exchange.termsOfServiceLastEtag;
+ const { exchange, exchangeDetails } = await this.updateExchangeFromUrl(
+ exchangeBaseUrl,
+ );
+ const tos = exchangeDetails.termsOfServiceText;
+ const currentEtag = exchangeDetails.termsOfServiceLastEtag;
if (!tos || !currentEtag) {
throw Error("exchange is in invalid state");
}
return {
- acceptedEtag: exchange.termsOfServiceAcceptedEtag,
+ acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
currentEtag,
tos,
};
@@ -678,28 +682,29 @@ export class Wallet {
}
async getExchanges(): Promise<ExchangesListRespose> {
- const exchanges: (ExchangeListItem | undefined)[] = await this.db
- .iter(Stores.exchanges)
- .map((x) => {
- const details = x.details;
- if (!details) {
- return undefined;
- }
- if (!x.addComplete) {
- return undefined;
- }
- if (!x.wireInfo) {
- return undefined;
- }
- return {
- exchangeBaseUrl: x.baseUrl,
- currency: details.currency,
- paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
- };
+ const exchangeRecords = await this.db.iter(Stores.exchanges).toArray();
+ const exchanges: ExchangeListItem[] = [];
+ for (const r of exchangeRecords) {
+ const dp = r.detailsPointer;
+ if (!dp) {
+ continue;
+ }
+ const { currency, masterPublicKey } = dp;
+ const exchangeDetails = await this.db.get(Stores.exchangeDetails, [
+ r.baseUrl,
+ currency,
+ masterPublicKey,
+ ]);
+ if (!exchangeDetails) {
+ continue;
+ }
+ exchanges.push({
+ exchangeBaseUrl: r.baseUrl,
+ currency,
+ paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
});
- return {
- exchanges: exchanges.filter((x) => !!x) as ExchangeListItem[],
- };
+ }
+ return { exchanges };
}
async getCurrencies(): Promise<WalletCurrencyInfo> {