support multiple exchange details per base URL
This commit is contained in:
parent
c6c17a1c0a
commit
02f1d4b081
@ -128,6 +128,8 @@ export interface WalletBackupContentV1 {
|
|||||||
*/
|
*/
|
||||||
exchanges: BackupExchange[];
|
exchanges: BackupExchange[];
|
||||||
|
|
||||||
|
exchange_details: BackupExchangeDetails[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grouped refresh sessions.
|
* 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 {
|
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.
|
* Canonicalized base url of the exchange.
|
||||||
*/
|
*/
|
||||||
@ -1158,11 +1185,6 @@ export interface BackupExchange {
|
|||||||
* ETag for last terms of service download.
|
* ETag for last terms of service download.
|
||||||
*/
|
*/
|
||||||
tos_etag_accepted: string | undefined;
|
tos_etag_accepted: string | undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Should this exchange be considered defective?
|
|
||||||
*/
|
|
||||||
defective?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BackupProposalStatus {
|
export enum BackupProposalStatus {
|
||||||
|
@ -598,10 +598,10 @@ advancedCli
|
|||||||
})
|
})
|
||||||
.action(async (args) => {
|
.action(async (args) => {
|
||||||
await withWallet(args, async (wallet) => {
|
await withWallet(args, async (wallet) => {
|
||||||
const exchange = await wallet.updateExchangeFromUrl(
|
const { exchange, exchangeDetails } = await wallet.updateExchangeFromUrl(
|
||||||
args.withdrawManually.exchange,
|
args.withdrawManually.exchange,
|
||||||
);
|
);
|
||||||
const acct = exchange.wireInfo?.accounts[0];
|
const acct = exchangeDetails.wireInfo.accounts[0];
|
||||||
if (!acct) {
|
if (!acct) {
|
||||||
console.log("exchange has no accounts");
|
console.log("exchange has no accounts");
|
||||||
return;
|
return;
|
||||||
|
@ -513,50 +513,6 @@ export interface DenominationRecord {
|
|||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Details about the exchange that we only know after
|
|
||||||
* querying /keys and /wire.
|
|
||||||
*/
|
|
||||||
export interface ExchangeDetails {
|
|
||||||
/**
|
|
||||||
* Master public key of the exchange.
|
|
||||||
*/
|
|
||||||
masterPublicKey: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auditors (partially) auditing the exchange.
|
|
||||||
*/
|
|
||||||
auditors: Auditor[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currency that the exchange offers.
|
|
||||||
*/
|
|
||||||
currency: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last observed protocol version.
|
|
||||||
*/
|
|
||||||
protocolVersion: string;
|
|
||||||
|
|
||||||
reserveClosingDelay: Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signing keys we got from the exchange, can also contain
|
|
||||||
* older signing keys that are not returned by /keys anymore.
|
|
||||||
*/
|
|
||||||
signingKeys: ExchangeSignKeyJson[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamp for last update.
|
|
||||||
*/
|
|
||||||
lastUpdateTime: Timestamp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When should we next update the information about the exchange?
|
|
||||||
*/
|
|
||||||
nextUpdateTime: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ExchangeUpdateStatus {
|
export enum ExchangeUpdateStatus {
|
||||||
FetchKeys = "fetch-keys",
|
FetchKeys = "fetch-keys",
|
||||||
FetchWire = "fetch-wire",
|
FetchWire = "fetch-wire",
|
||||||
@ -570,50 +526,42 @@ export interface ExchangeBankAccount {
|
|||||||
master_sig: string;
|
master_sig: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeWireInfo {
|
|
||||||
feesForType: { [wireMethod: string]: WireFee[] };
|
|
||||||
accounts: ExchangeBankAccount[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ExchangeUpdateReason {
|
export enum ExchangeUpdateReason {
|
||||||
Initial = "initial",
|
Initial = "initial",
|
||||||
Forced = "forced",
|
Forced = "forced",
|
||||||
Scheduled = "scheduled",
|
Scheduled = "scheduled",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface ExchangeDetailsRecord {
|
||||||
* Exchange record as stored in the wallet's database.
|
|
||||||
*/
|
|
||||||
export interface ExchangeRecord {
|
|
||||||
/**
|
/**
|
||||||
* Base url of the exchange.
|
* Master public key of the exchange.
|
||||||
*/
|
*/
|
||||||
baseUrl: string;
|
masterPublicKey: string;
|
||||||
|
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Did we finish adding the exchange?
|
* Currency that the exchange offers.
|
||||||
*/
|
*/
|
||||||
addComplete: boolean;
|
currency: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this a permanent or temporary exchange record?
|
* Auditors (partially) auditing the exchange.
|
||||||
*/
|
*/
|
||||||
permanent: boolean;
|
auditors: Auditor[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Was the exchange added as a built-in exchange?
|
* Last observed protocol version.
|
||||||
*/
|
*/
|
||||||
builtIn: boolean;
|
protocolVersion: string;
|
||||||
|
|
||||||
|
reserveClosingDelay: Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Details, once known.
|
* Signing keys we got from the exchange, can also contain
|
||||||
|
* older signing keys that are not returned by /keys anymore.
|
||||||
*/
|
*/
|
||||||
details: ExchangeDetails | undefined;
|
signingKeys: ExchangeSignKeyJson[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping from wire method type to the wire fee.
|
|
||||||
*/
|
|
||||||
wireInfo: ExchangeWireInfo | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Terms of service text or undefined if not downloaded yet.
|
* Terms of service text or undefined if not downloaded yet.
|
||||||
@ -632,6 +580,52 @@ export interface ExchangeRecord {
|
|||||||
*/
|
*/
|
||||||
termsOfServiceAcceptedEtag: string | undefined;
|
termsOfServiceAcceptedEtag: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp for last update.
|
||||||
|
*/
|
||||||
|
lastUpdateTime: Timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When should we next update the information about the exchange?
|
||||||
|
*/
|
||||||
|
nextUpdateTime: Timestamp;
|
||||||
|
|
||||||
|
wireInfo: WireInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WireInfo {
|
||||||
|
feesForType: { [wireMethod: string]: WireFee[] };
|
||||||
|
|
||||||
|
accounts: ExchangeBankAccount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeDetailsPointer {
|
||||||
|
masterPublicKey: string;
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the (masterPublicKey, currency) pointer
|
||||||
|
* has been updated.
|
||||||
|
*/
|
||||||
|
updateClock: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange record as stored in the wallet's database.
|
||||||
|
*/
|
||||||
|
export interface ExchangeRecord {
|
||||||
|
/**
|
||||||
|
* Base url of the exchange.
|
||||||
|
*/
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
detailsPointer: ExchangeDetailsPointer | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this a permanent or temporary exchange record?
|
||||||
|
*/
|
||||||
|
permanent: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the update to the exchange has been started or
|
* Time when the update to the exchange has been started or
|
||||||
* undefined if no update is in progress.
|
* undefined if no update is in progress.
|
||||||
@ -640,6 +634,9 @@ export interface ExchangeRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of updating the info about the exchange.
|
* Status of updating the info about the exchange.
|
||||||
|
*
|
||||||
|
* FIXME: Adapt this to recent changes regarding how
|
||||||
|
* updating exchange details works.
|
||||||
*/
|
*/
|
||||||
updateStatus: ExchangeUpdateStatus;
|
updateStatus: ExchangeUpdateStatus;
|
||||||
|
|
||||||
@ -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> {
|
class CoinsStore extends Store<"coins", CoinRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("coins", { keyPath: "coinPub" });
|
super("coins", { keyPath: "coinPub" });
|
||||||
@ -1924,6 +1932,7 @@ export const Stores = {
|
|||||||
exchangeTrustStore: new ExchangeTrustStore(),
|
exchangeTrustStore: new ExchangeTrustStore(),
|
||||||
denominations: new DenominationsStore(),
|
denominations: new DenominationsStore(),
|
||||||
exchanges: new ExchangesStore(),
|
exchanges: new ExchangesStore(),
|
||||||
|
exchangeDetails: new ExchangeDetailsStore(),
|
||||||
proposals: new ProposalsStore(),
|
proposals: new ProposalsStore(),
|
||||||
refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
|
refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
|
||||||
"refreshGroups",
|
"refreshGroups",
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
BackupProposalStatus,
|
BackupProposalStatus,
|
||||||
BackupRefreshOldCoin,
|
BackupRefreshOldCoin,
|
||||||
BackupRefreshSession,
|
BackupRefreshSession,
|
||||||
|
BackupExchangeDetails,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { InternalWalletState } from "../state";
|
import { InternalWalletState } from "../state";
|
||||||
import {
|
import {
|
||||||
@ -65,6 +66,7 @@ import {
|
|||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
|
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
|
||||||
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
|
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
|
||||||
|
import { getExchangeDetails } from "../exchanges.js";
|
||||||
|
|
||||||
export async function exportBackup(
|
export async function exportBackup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -74,6 +76,7 @@ export async function exportBackup(
|
|||||||
[
|
[
|
||||||
Stores.config,
|
Stores.config,
|
||||||
Stores.exchanges,
|
Stores.exchanges,
|
||||||
|
Stores.exchangeDetails,
|
||||||
Stores.coins,
|
Stores.coins,
|
||||||
Stores.denominations,
|
Stores.denominations,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
@ -88,6 +91,7 @@ export async function exportBackup(
|
|||||||
async (tx) => {
|
async (tx) => {
|
||||||
const bs = await getWalletBackupState(ws, tx);
|
const bs = await getWalletBackupState(ws, tx);
|
||||||
|
|
||||||
|
const backupExchangeDetails: BackupExchangeDetails[] = [];
|
||||||
const backupExchanges: BackupExchange[] = [];
|
const backupExchanges: BackupExchange[] = [];
|
||||||
const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
|
const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
|
||||||
const backupDenominationsByExchange: {
|
const backupDenominationsByExchange: {
|
||||||
@ -254,21 +258,22 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.exchanges).forEach((ex) => {
|
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.
|
// Only back up permanently added exchanges.
|
||||||
|
|
||||||
if (!ex.details) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ex.wireInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ex.addComplete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ex.permanent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const wi = ex.wireInfo;
|
const wi = ex.wireInfo;
|
||||||
const wireFees: BackupExchangeWireFee[] = [];
|
const wireFees: BackupExchangeWireFee[] = [];
|
||||||
|
|
||||||
@ -285,23 +290,23 @@ export async function exportBackup(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
backupExchanges.push({
|
backupExchangeDetails.push({
|
||||||
base_url: ex.baseUrl,
|
base_url: ex.exchangeBaseUrl,
|
||||||
reserve_closing_delay: ex.details.reserveClosingDelay,
|
reserve_closing_delay: ex.reserveClosingDelay,
|
||||||
accounts: ex.wireInfo.accounts.map((x) => ({
|
accounts: ex.wireInfo.accounts.map((x) => ({
|
||||||
payto_uri: x.payto_uri,
|
payto_uri: x.payto_uri,
|
||||||
master_sig: x.master_sig,
|
master_sig: x.master_sig,
|
||||||
})),
|
})),
|
||||||
auditors: ex.details.auditors.map((x) => ({
|
auditors: ex.auditors.map((x) => ({
|
||||||
auditor_pub: x.auditor_pub,
|
auditor_pub: x.auditor_pub,
|
||||||
auditor_url: x.auditor_url,
|
auditor_url: x.auditor_url,
|
||||||
denomination_keys: x.denomination_keys,
|
denomination_keys: x.denomination_keys,
|
||||||
})),
|
})),
|
||||||
master_public_key: ex.details.masterPublicKey,
|
master_public_key: ex.masterPublicKey,
|
||||||
currency: ex.details.currency,
|
currency: ex.currency,
|
||||||
protocol_version: ex.details.protocolVersion,
|
protocol_version: ex.protocolVersion,
|
||||||
wire_fees: wireFees,
|
wire_fees: wireFees,
|
||||||
signing_keys: ex.details.signingKeys.map((x) => ({
|
signing_keys: ex.signingKeys.map((x) => ({
|
||||||
key: x.key,
|
key: x.key,
|
||||||
master_sig: x.master_sig,
|
master_sig: x.master_sig,
|
||||||
stamp_end: x.stamp_end,
|
stamp_end: x.stamp_end,
|
||||||
@ -310,8 +315,9 @@ export async function exportBackup(
|
|||||||
})),
|
})),
|
||||||
tos_etag_accepted: ex.termsOfServiceAcceptedEtag,
|
tos_etag_accepted: ex.termsOfServiceAcceptedEtag,
|
||||||
tos_etag_last: ex.termsOfServiceLastEtag,
|
tos_etag_last: ex.termsOfServiceLastEtag,
|
||||||
denominations: backupDenominationsByExchange[ex.baseUrl] ?? [],
|
denominations:
|
||||||
reserves: backupReservesByExchange[ex.baseUrl] ?? [],
|
backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
|
||||||
|
reserves: backupReservesByExchange[ex.exchangeBaseUrl] ?? [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -451,6 +457,7 @@ export async function exportBackup(
|
|||||||
schema_id: "gnu-taler-wallet-backup-content",
|
schema_id: "gnu-taler-wallet-backup-content",
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
exchanges: backupExchanges,
|
exchanges: backupExchanges,
|
||||||
|
exchange_details: backupExchangeDetails,
|
||||||
wallet_root_pub: bs.walletRootPub,
|
wallet_root_pub: bs.walletRootPub,
|
||||||
backup_providers: backupBackupProviders,
|
backup_providers: backupBackupProviders,
|
||||||
current_device_id: bs.deviceId,
|
current_device_id: bs.deviceId,
|
||||||
|
@ -32,7 +32,6 @@ import {
|
|||||||
Stores,
|
Stores,
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
ExchangeWireInfo,
|
|
||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
DenominationStatus,
|
DenominationStatus,
|
||||||
CoinSource,
|
CoinSource,
|
||||||
@ -46,6 +45,7 @@ import {
|
|||||||
RefundState,
|
RefundState,
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
|
WireInfo,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { TransactionHandle } from "../../index.js";
|
import { TransactionHandle } from "../../index.js";
|
||||||
import { PayCoinSelection } from "../../util/coinSelection";
|
import { PayCoinSelection } from "../../util/coinSelection";
|
||||||
@ -56,6 +56,7 @@ import { initRetryInfo } from "../../util/retries";
|
|||||||
import { InternalWalletState } from "../state";
|
import { InternalWalletState } from "../state";
|
||||||
import { provideBackupState } from "./state";
|
import { provideBackupState } from "./state";
|
||||||
import { makeEventId, TombstoneTag } from "../transactions.js";
|
import { makeEventId, TombstoneTag } from "../transactions.js";
|
||||||
|
import { getExchangeDetails } from "../exchanges.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/backup/import.ts");
|
const logger = new Logger("operations/backup/import.ts");
|
||||||
|
|
||||||
@ -102,13 +103,13 @@ async function recoverPayCoinSelection(
|
|||||||
totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
|
totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
|
||||||
|
|
||||||
if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
|
if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
|
||||||
const exchange = await tx.get(
|
const exchangeDetails = await getExchangeDetails(
|
||||||
Stores.exchanges,
|
tx,
|
||||||
coinRecord.exchangeBaseUrl,
|
coinRecord.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
checkBackupInvariant(!!exchange);
|
checkBackupInvariant(!!exchangeDetails);
|
||||||
let wireFee: AmountJson | undefined;
|
let wireFee: AmountJson | undefined;
|
||||||
const feesForType = exchange.wireInfo?.feesForType;
|
const feesForType = exchangeDetails.wireInfo.feesForType;
|
||||||
checkBackupInvariant(!!feesForType);
|
checkBackupInvariant(!!feesForType);
|
||||||
for (const fee of feesForType[contractData.wireMethod] || []) {
|
for (const fee of feesForType[contractData.wireMethod] || []) {
|
||||||
if (
|
if (
|
||||||
@ -218,6 +219,7 @@ export async function importBackup(
|
|||||||
[
|
[
|
||||||
Stores.config,
|
Stores.config,
|
||||||
Stores.exchanges,
|
Stores.exchanges,
|
||||||
|
Stores.exchangeDetails,
|
||||||
Stores.coins,
|
Stores.coins,
|
||||||
Stores.denominations,
|
Stores.denominations,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
@ -245,21 +247,46 @@ export async function importBackup(
|
|||||||
|
|
||||||
const tombstoneSet = new Set(backupBlob.tombstones);
|
const tombstoneSet = new Set(backupBlob.tombstones);
|
||||||
|
|
||||||
|
// FIXME: Validate that the "details pointer" is correct
|
||||||
|
|
||||||
for (const backupExchange of backupBlob.exchanges) {
|
for (const backupExchange of backupBlob.exchanges) {
|
||||||
const existingExchange = await tx.get(
|
const existingExchange = await tx.get(
|
||||||
Stores.exchanges,
|
Stores.exchanges,
|
||||||
backupExchange.base_url,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!existingExchange) {
|
for (const backupExchangeDetails of backupBlob.exchange_details) {
|
||||||
const wireInfo: ExchangeWireInfo = {
|
const existingExchangeDetails = await tx.get(Stores.exchangeDetails, [
|
||||||
accounts: backupExchange.accounts.map((x) => ({
|
backupExchangeDetails.base_url,
|
||||||
|
backupExchangeDetails.currency,
|
||||||
|
backupExchangeDetails.master_public_key,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!existingExchangeDetails) {
|
||||||
|
const wireInfo: WireInfo = {
|
||||||
|
accounts: backupExchangeDetails.accounts.map((x) => ({
|
||||||
master_sig: x.master_sig,
|
master_sig: x.master_sig,
|
||||||
payto_uri: x.payto_uri,
|
payto_uri: x.payto_uri,
|
||||||
})),
|
})),
|
||||||
feesForType: {},
|
feesForType: {},
|
||||||
};
|
};
|
||||||
for (const fee of backupExchange.wire_fees) {
|
for (const fee of backupExchangeDetails.wire_fees) {
|
||||||
const w = (wireInfo.feesForType[fee.wire_type] ??= []);
|
const w = (wireInfo.feesForType[fee.wire_type] ??= []);
|
||||||
w.push({
|
w.push({
|
||||||
closingFee: Amounts.parseOrThrow(fee.closing_fee),
|
closingFee: Amounts.parseOrThrow(fee.closing_fee),
|
||||||
@ -269,48 +296,39 @@ export async function importBackup(
|
|||||||
wireFee: Amounts.parseOrThrow(fee.wire_fee),
|
wireFee: Amounts.parseOrThrow(fee.wire_fee),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await tx.put(Stores.exchanges, {
|
await tx.put(Stores.exchangeDetails, {
|
||||||
addComplete: true,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
baseUrl: backupExchange.base_url,
|
termsOfServiceAcceptedEtag: backupExchangeDetails.tos_etag_accepted,
|
||||||
builtIn: false,
|
|
||||||
updateReason: undefined,
|
|
||||||
permanent: true,
|
|
||||||
retryInfo: initRetryInfo(),
|
|
||||||
termsOfServiceAcceptedEtag: backupExchange.tos_etag_accepted,
|
|
||||||
termsOfServiceText: undefined,
|
termsOfServiceText: undefined,
|
||||||
termsOfServiceLastEtag: backupExchange.tos_etag_last,
|
termsOfServiceLastEtag: backupExchangeDetails.tos_etag_last,
|
||||||
updateStarted: getTimestampNow(),
|
|
||||||
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
|
||||||
wireInfo,
|
wireInfo,
|
||||||
details: {
|
currency: backupExchangeDetails.currency,
|
||||||
currency: backupExchange.currency,
|
auditors: backupExchangeDetails.auditors.map((x) => ({
|
||||||
reserveClosingDelay: backupExchange.reserve_closing_delay,
|
auditor_pub: x.auditor_pub,
|
||||||
auditors: backupExchange.auditors.map((x) => ({
|
auditor_url: x.auditor_url,
|
||||||
auditor_pub: x.auditor_pub,
|
denomination_keys: x.denomination_keys,
|
||||||
auditor_url: x.auditor_url,
|
})),
|
||||||
denomination_keys: x.denomination_keys,
|
lastUpdateTime: { t_ms: "never" },
|
||||||
})),
|
masterPublicKey: backupExchangeDetails.master_public_key,
|
||||||
lastUpdateTime: { t_ms: "never" },
|
nextUpdateTime: { t_ms: "never" },
|
||||||
masterPublicKey: backupExchange.master_public_key,
|
protocolVersion: backupExchangeDetails.protocol_version,
|
||||||
nextUpdateTime: { t_ms: "never" },
|
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
|
||||||
protocolVersion: backupExchange.protocol_version,
|
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
|
||||||
signingKeys: backupExchange.signing_keys.map((x) => ({
|
key: x.key,
|
||||||
key: x.key,
|
master_sig: x.master_sig,
|
||||||
master_sig: x.master_sig,
|
stamp_end: x.stamp_end,
|
||||||
stamp_end: x.stamp_end,
|
stamp_expire: x.stamp_expire,
|
||||||
stamp_expire: x.stamp_expire,
|
stamp_start: x.stamp_start,
|
||||||
stamp_start: x.stamp_start,
|
})),
|
||||||
})),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const backupDenomination of backupExchange.denominations) {
|
for (const backupDenomination of backupExchangeDetails.denominations) {
|
||||||
const denomPubHash =
|
const denomPubHash =
|
||||||
cryptoComp.denomPubToHash[backupDenomination.denom_pub];
|
cryptoComp.denomPubToHash[backupDenomination.denom_pub];
|
||||||
checkLogicInvariant(!!denomPubHash);
|
checkLogicInvariant(!!denomPubHash);
|
||||||
const existingDenom = await tx.get(Stores.denominations, [
|
const existingDenom = await tx.get(Stores.denominations, [
|
||||||
backupExchange.base_url,
|
backupExchangeDetails.base_url,
|
||||||
denomPubHash,
|
denomPubHash,
|
||||||
]);
|
]);
|
||||||
if (!existingDenom) {
|
if (!existingDenom) {
|
||||||
@ -321,7 +339,7 @@ export async function importBackup(
|
|||||||
await tx.put(Stores.denominations, {
|
await tx.put(Stores.denominations, {
|
||||||
denomPub: backupDenomination.denom_pub,
|
denomPub: backupDenomination.denom_pub,
|
||||||
denomPubHash: denomPubHash,
|
denomPubHash: denomPubHash,
|
||||||
exchangeBaseUrl: backupExchange.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
|
feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
|
||||||
feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
|
feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
|
||||||
feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
|
feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
|
||||||
@ -378,7 +396,7 @@ export async function importBackup(
|
|||||||
denomSig: backupCoin.denom_sig,
|
denomSig: backupCoin.denom_sig,
|
||||||
coinPub: compCoin.coinPub,
|
coinPub: compCoin.coinPub,
|
||||||
suspended: false,
|
suspended: false,
|
||||||
exchangeBaseUrl: backupExchange.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
denomPub: backupDenomination.denom_pub,
|
denomPub: backupDenomination.denom_pub,
|
||||||
denomPubHash,
|
denomPubHash,
|
||||||
status: backupCoin.fresh
|
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 =
|
const reservePub =
|
||||||
cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
|
cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
|
||||||
const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
|
const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
|
||||||
@ -414,7 +432,7 @@ export async function importBackup(
|
|||||||
await tx.put(Stores.reserves, {
|
await tx.put(Stores.reserves, {
|
||||||
currency: instructedAmount.currency,
|
currency: instructedAmount.currency,
|
||||||
instructedAmount,
|
instructedAmount,
|
||||||
exchangeBaseUrl: backupExchange.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
reservePub,
|
reservePub,
|
||||||
reservePriv: backupReserve.reserve_priv,
|
reservePriv: backupReserve.reserve_priv,
|
||||||
requestedQuery: false,
|
requestedQuery: false,
|
||||||
@ -436,7 +454,7 @@ export async function importBackup(
|
|||||||
reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
|
reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
|
||||||
initialDenomSel: await getDenomSelStateFromBackup(
|
initialDenomSel: await getDenomSelStateFromBackup(
|
||||||
tx,
|
tx,
|
||||||
backupExchange.base_url,
|
backupExchangeDetails.base_url,
|
||||||
backupReserve.initial_selected_denoms,
|
backupReserve.initial_selected_denoms,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -457,10 +475,10 @@ export async function importBackup(
|
|||||||
await tx.put(Stores.withdrawalGroups, {
|
await tx.put(Stores.withdrawalGroups, {
|
||||||
denomsSel: await getDenomSelStateFromBackup(
|
denomsSel: await getDenomSelStateFromBackup(
|
||||||
tx,
|
tx,
|
||||||
backupExchange.base_url,
|
backupExchangeDetails.base_url,
|
||||||
backupWg.selected_denoms,
|
backupWg.selected_denoms,
|
||||||
),
|
),
|
||||||
exchangeBaseUrl: backupExchange.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
rawWithdrawalAmount: Amounts.parseOrThrow(
|
rawWithdrawalAmount: Amounts.parseOrThrow(
|
||||||
backupWg.raw_withdrawal_amount,
|
backupWg.raw_withdrawal_amount,
|
||||||
|
@ -155,8 +155,8 @@ async function computeBackupCryptoData(
|
|||||||
proposalNoncePrivToPub: {},
|
proposalNoncePrivToPub: {},
|
||||||
reservePrivToPub: {},
|
reservePrivToPub: {},
|
||||||
};
|
};
|
||||||
for (const backupExchange of backupContent.exchanges) {
|
for (const backupExchangeDetails of backupContent.exchange_details) {
|
||||||
for (const backupDenom of backupExchange.denominations) {
|
for (const backupDenom of backupExchangeDetails.denominations) {
|
||||||
for (const backupCoin of backupDenom.coins) {
|
for (const backupCoin of backupDenom.coins) {
|
||||||
const coinPub = encodeCrock(
|
const coinPub = encodeCrock(
|
||||||
eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
|
eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
|
||||||
@ -175,7 +175,7 @@ async function computeBackupCryptoData(
|
|||||||
hash(decodeCrock(backupDenom.denom_pub)),
|
hash(decodeCrock(backupDenom.denom_pub)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const backupReserve of backupExchange.reserves) {
|
for (const backupReserve of backupExchangeDetails.reserves) {
|
||||||
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
|
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
|
||||||
eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
|
eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ExchangeRecord, Stores } from "../db.js";
|
import { ExchangeRecord, Stores } from "../db.js";
|
||||||
import { Logger } from "../index.js";
|
import { Logger } from "../index.js";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { InternalWalletState } from "./state.js";
|
import { InternalWalletState } from "./state.js";
|
||||||
|
|
||||||
const logger = new Logger("currencies.ts");
|
const logger = new Logger("currencies.ts");
|
||||||
@ -37,7 +38,12 @@ export async function getExchangeTrust(
|
|||||||
): Promise<TrustInfo> {
|
): Promise<TrustInfo> {
|
||||||
let isTrusted = false;
|
let isTrusted = false;
|
||||||
let isAudited = 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) {
|
if (!exchangeDetails) {
|
||||||
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
|
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ import { InternalWalletState } from "./state";
|
|||||||
import { Logger } from "../util/logging.js";
|
import { Logger } from "../util/logging.js";
|
||||||
import { DepositGroupRecord, Stores } from "../db.js";
|
import { DepositGroupRecord, Stores } from "../db.js";
|
||||||
import { guardOperationException } from "./errors.js";
|
import { guardOperationException } from "./errors.js";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -308,14 +309,17 @@ export async function createDepositGroup(
|
|||||||
const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
|
const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
|
||||||
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
||||||
for (const e of allExchanges) {
|
for (const e of allExchanges) {
|
||||||
if (!e.details) {
|
const details = await ws.db.runWithReadTransaction(
|
||||||
continue;
|
[Stores.exchanges, Stores.exchangeDetails],
|
||||||
}
|
async (tx) => {
|
||||||
if (e.details.currency != amount.currency) {
|
return getExchangeDetails(tx, e.baseUrl);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!details) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
exchangeInfos.push({
|
exchangeInfos.push({
|
||||||
master_pub: e.details.masterPublicKey,
|
master_pub: details.masterPublicKey,
|
||||||
url: e.baseUrl,
|
url: e.baseUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -19,18 +19,23 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Amounts,
|
Amounts,
|
||||||
|
Auditor,
|
||||||
codecForExchangeKeysJson,
|
codecForExchangeKeysJson,
|
||||||
codecForExchangeWireJson,
|
codecForExchangeWireJson,
|
||||||
compare,
|
compare,
|
||||||
Denomination,
|
Denomination,
|
||||||
Duration,
|
Duration,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
|
ExchangeSignKeyJson,
|
||||||
|
ExchangeWireJson,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
isTimestampExpired,
|
isTimestampExpired,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
|
Recoup,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetails,
|
TalerErrorDetails,
|
||||||
|
Timestamp,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
@ -40,6 +45,8 @@ import {
|
|||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
WireFee,
|
WireFee,
|
||||||
ExchangeUpdateReason,
|
ExchangeUpdateReason,
|
||||||
|
ExchangeDetailsRecord,
|
||||||
|
WireInfo,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
Logger,
|
Logger,
|
||||||
@ -47,14 +54,16 @@ import {
|
|||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
getExpiryTimestamp,
|
getExpiryTimestamp,
|
||||||
readSuccessResponseTextOrThrow,
|
readSuccessResponseTextOrThrow,
|
||||||
|
encodeCrock,
|
||||||
|
hash,
|
||||||
|
decodeCrock,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
|
||||||
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js";
|
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
makeErrorDetails,
|
makeErrorDetails,
|
||||||
OperationFailedAndReportedError,
|
|
||||||
guardOperationException,
|
guardOperationException,
|
||||||
|
OperationFailedError,
|
||||||
} from "./errors.js";
|
} from "./errors.js";
|
||||||
import { createRecoupGroup, processRecoupGroup } from "./recoup.js";
|
import { createRecoupGroup, processRecoupGroup } from "./recoup.js";
|
||||||
import { InternalWalletState } from "./state.js";
|
import { InternalWalletState } from "./state.js";
|
||||||
@ -62,15 +71,17 @@ import {
|
|||||||
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
} from "./versions.js";
|
} 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");
|
const logger = new Logger("exchanges.ts");
|
||||||
|
|
||||||
async function denominationRecordFromKeys(
|
function denominationRecordFromKeys(
|
||||||
ws: InternalWalletState,
|
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
denomIn: Denomination,
|
denomIn: Denomination,
|
||||||
): Promise<DenominationRecord> {
|
): DenominationRecord {
|
||||||
const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
|
const denomPubHash = encodeCrock(hash(decodeCrock(denomIn.denom_pub)));
|
||||||
const d: DenominationRecord = {
|
const d: DenominationRecord = {
|
||||||
denomPub: denomIn.denom_pub,
|
denomPub: denomIn.denom_pub,
|
||||||
denomPubHash,
|
denomPubHash,
|
||||||
@ -115,29 +126,206 @@ function getExchangeRequestTimeout(e: ExchangeRecord): Duration {
|
|||||||
return { d_ms: 5000 };
|
return { d_ms: 5000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface ExchangeTosDownloadResult {
|
||||||
* Fetch the exchange's /keys and update our database accordingly.
|
tosText: string;
|
||||||
*
|
tosEtag: string;
|
||||||
* Exceptions thrown in this method must be caught and reported
|
}
|
||||||
* in the pending operations.
|
|
||||||
*/
|
|
||||||
async function updateExchangeWithKeys(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
baseUrl: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
|
|
||||||
|
|
||||||
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const dp = r.detailsPointer;
|
||||||
|
if (!dp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { currency, masterPublicKey } = dp;
|
||||||
|
return await tx.get(Stores.exchangeDetails, [
|
||||||
|
r.baseUrl,
|
||||||
|
currency,
|
||||||
|
masterPublicKey,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("updating exchange /keys info");
|
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 wire information for an exchange.
|
||||||
|
*
|
||||||
|
* @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
const keysUrl = new URL("keys", baseUrl);
|
||||||
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||||
|
|
||||||
const resp = await ws.http.get(keysUrl.href, {
|
const resp = await http.get(keysUrl.href, {
|
||||||
timeout: getExchangeRequestTimeout(existingExchangeRecord),
|
timeout,
|
||||||
});
|
});
|
||||||
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
|
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
@ -155,8 +343,7 @@ async function updateExchangeWithKeys(
|
|||||||
exchangeBaseUrl: baseUrl,
|
exchangeBaseUrl: baseUrl,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await handleExchangeUpdateError(ws, baseUrl, opErr);
|
throw new OperationFailedError(opErr);
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolVersion = exchangeKeysJson.version;
|
const protocolVersion = exchangeKeysJson.version;
|
||||||
@ -171,70 +358,138 @@ async function updateExchangeWithKeys(
|
|||||||
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await handleExchangeUpdateError(ws, baseUrl, opErr);
|
throw new OperationFailedError(opErr);
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
|
const currency = Amounts.parseOrThrow(
|
||||||
.currency;
|
exchangeKeysJson.denoms[0].value,
|
||||||
|
).currency.toUpperCase();
|
||||||
|
|
||||||
logger.trace("processing denominations");
|
return {
|
||||||
|
masterPublicKey: exchangeKeysJson.master_public_key,
|
||||||
const newDenominations = await Promise.all(
|
currency,
|
||||||
exchangeKeysJson.denoms.map((d) =>
|
auditors: exchangeKeysJson.auditors,
|
||||||
denominationRecordFromKeys(ws, baseUrl, d),
|
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(
|
const updated = await ws.db.runWithWriteTransaction(
|
||||||
[Stores.exchanges, Stores.denominations, Stores.recoupGroups, Stores.coins],
|
[
|
||||||
|
Stores.exchanges,
|
||||||
|
Stores.exchangeDetails,
|
||||||
|
Stores.denominations,
|
||||||
|
Stores.recoupGroups,
|
||||||
|
Stores.coins,
|
||||||
|
],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const r = await tx.get(Stores.exchanges, baseUrl);
|
const r = await tx.get(Stores.exchanges, baseUrl);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
logger.warn(`exchange ${baseUrl} no longer present`);
|
logger.warn(`exchange ${baseUrl} no longer present`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (r.details) {
|
let details = await getExchangeDetails(tx, r.baseUrl);
|
||||||
|
if (details) {
|
||||||
// FIXME: We need to do some consistency checks!
|
// FIXME: We need to do some consistency checks!
|
||||||
}
|
}
|
||||||
// FIXME: validate signing keys and merge with old set
|
// FIXME: validate signing keys and merge with old set
|
||||||
r.details = {
|
details = {
|
||||||
auditors: exchangeKeysJson.auditors,
|
auditors: keysInfo.auditors,
|
||||||
currency: currency,
|
currency: keysInfo.currency,
|
||||||
lastUpdateTime: lastUpdateTimestamp,
|
lastUpdateTime: now,
|
||||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
masterPublicKey: keysInfo.masterPublicKey,
|
||||||
protocolVersion: protocolVersion,
|
protocolVersion: keysInfo.protocolVersion,
|
||||||
signingKeys: exchangeKeysJson.signkeys,
|
signingKeys: keysInfo.signingKeys,
|
||||||
nextUpdateTime: getExpiryTimestamp(resp, {
|
nextUpdateTime: keysInfo.expiry,
|
||||||
minDuration: durationFromSpec({ hours: 1 }),
|
reserveClosingDelay: keysInfo.reserveClosingDelay,
|
||||||
}),
|
exchangeBaseUrl: r.baseUrl,
|
||||||
reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
|
wireInfo,
|
||||||
|
termsOfServiceText: tosDownload.tosText,
|
||||||
|
termsOfServiceAcceptedEtag: undefined,
|
||||||
|
termsOfServiceLastEtag: tosDownload.tosEtag,
|
||||||
};
|
};
|
||||||
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
||||||
|
// FIXME: only update if pointer got updated
|
||||||
r.lastError = undefined;
|
r.lastError = undefined;
|
||||||
r.retryInfo = initRetryInfo(false);
|
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.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, [
|
const oldDenom = await tx.get(Stores.denominations, [
|
||||||
baseUrl,
|
baseUrl,
|
||||||
newDenom.denomPubHash,
|
currentDenom.denomPubHash,
|
||||||
]);
|
]);
|
||||||
if (oldDenom) {
|
if (oldDenom) {
|
||||||
// FIXME: Do consistency check
|
// FIXME: Do consistency check
|
||||||
} else {
|
} else {
|
||||||
await tx.put(Stores.denominations, newDenom);
|
await tx.put(Stores.denominations, currentDenom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle recoup
|
// Handle recoup
|
||||||
const recoupDenomList = exchangeKeysJson.recoup ?? [];
|
const recoupDenomList = keysInfo.recoup;
|
||||||
const newlyRevokedCoinPubs: string[] = [];
|
const newlyRevokedCoinPubs: string[] = [];
|
||||||
logger.trace("recoup list from exchange", recoupDenomList);
|
logger.trace("recoup list from exchange", recoupDenomList);
|
||||||
for (const recoupInfo of recoupDenomList) {
|
for (const recoupInfo of recoupDenomList) {
|
||||||
@ -264,8 +519,12 @@ async function updateExchangeWithKeys(
|
|||||||
}
|
}
|
||||||
if (newlyRevokedCoinPubs.length != 0) {
|
if (newlyRevokedCoinPubs.length != 0) {
|
||||||
logger.trace("recouping coins", newlyRevokedCoinPubs);
|
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");
|
if (!updated) {
|
||||||
}
|
throw Error("something went wrong with updating the exchange");
|
||||||
|
}
|
||||||
|
|
||||||
async function updateExchangeFinalize(
|
return {
|
||||||
ws: InternalWalletState,
|
exchange: updated.exchange,
|
||||||
exchangeBaseUrl: string,
|
exchangeDetails: updated.exchangeDetails,
|
||||||
): Promise<void> {
|
|
||||||
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
|
||||||
if (!exchange) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
export async function getExchangePaytoUri(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
@ -535,15 +553,14 @@ export async function getExchangePaytoUri(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// We do the update here, since the exchange might not even exist
|
// We do the update here, since the exchange might not even exist
|
||||||
// yet in our database.
|
// yet in our database.
|
||||||
const exchangeRecord = await updateExchangeFromUrl(ws, exchangeBaseUrl);
|
const details = await ws.db.runWithReadTransaction(
|
||||||
if (!exchangeRecord) {
|
[Stores.exchangeDetails, Stores.exchanges],
|
||||||
throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
|
async (tx) => {
|
||||||
}
|
return getExchangeDetails(tx, exchangeBaseUrl);
|
||||||
const exchangeWireInfo = exchangeRecord.wireInfo;
|
},
|
||||||
if (!exchangeWireInfo) {
|
);
|
||||||
throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
|
const accounts = details?.wireInfo.accounts ?? [];
|
||||||
}
|
for (const account of accounts) {
|
||||||
for (const account of exchangeWireInfo.accounts) {
|
|
||||||
const res = parsePaytoUri(account.payto_uri);
|
const res = parsePaytoUri(account.payto_uri);
|
||||||
if (!res) {
|
if (!res) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -94,6 +94,7 @@ import {
|
|||||||
import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
|
import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
|
||||||
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js";
|
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js";
|
||||||
import { ContractTermsUtil } from "../util/contractTerms.js";
|
import { ContractTermsUtil } from "../util/contractTerms.js";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -170,11 +171,16 @@ export async function getEffectiveDepositAmount(
|
|||||||
exchangeSet.add(coin.exchangeBaseUrl);
|
exchangeSet.add(coin.exchangeBaseUrl);
|
||||||
}
|
}
|
||||||
for (const exchangeUrl of exchangeSet.values()) {
|
for (const exchangeUrl of exchangeSet.values()) {
|
||||||
const exchange = await ws.db.get(Stores.exchanges, exchangeUrl);
|
const exchangeDetails = await ws.db.runWithReadTransaction(
|
||||||
if (!exchange?.wireInfo) {
|
[Stores.exchanges, Stores.exchangeDetails],
|
||||||
|
async (tx) => {
|
||||||
|
return getExchangeDetails(tx, exchangeUrl);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!exchangeDetails) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fee = exchange.wireInfo.feesForType[wireType].find((x) => {
|
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
|
||||||
return timestampIsBetween(getTimestampNow(), x.startStamp, x.endStamp);
|
return timestampIsBetween(getTimestampNow(), x.startStamp, x.endStamp);
|
||||||
})?.wireFee;
|
})?.wireFee;
|
||||||
if (fee) {
|
if (fee) {
|
||||||
@ -240,11 +246,16 @@ export async function getCandidatePayCoins(
|
|||||||
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
|
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
|
||||||
for (const exchange of exchanges) {
|
for (const exchange of exchanges) {
|
||||||
let isOkay = false;
|
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) {
|
if (!exchangeDetails) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const exchangeFees = exchange.wireInfo;
|
const exchangeFees = exchangeDetails.wireInfo;
|
||||||
if (!exchangeFees) {
|
if (!exchangeFees) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,10 @@ import {
|
|||||||
getDurationRemaining,
|
getDurationRemaining,
|
||||||
durationMin,
|
durationMin,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { Store, TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { getBalancesInsideTransaction } from "./balance";
|
import { getBalancesInsideTransaction } from "./balance";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
|
||||||
function updateRetryDelay(
|
function updateRetryDelay(
|
||||||
oldDelay: Duration,
|
oldDelay: Duration,
|
||||||
@ -52,12 +53,14 @@ function updateRetryDelay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherExchangePending(
|
async function gatherExchangePending(
|
||||||
tx: TransactionHandle<typeof Stores.exchanges>,
|
tx: TransactionHandle<
|
||||||
|
typeof Stores.exchanges | typeof Stores.exchangeDetails
|
||||||
|
>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.exchanges).forEach((e) => {
|
await tx.iter(Stores.exchanges).forEachAsync(async (e) => {
|
||||||
switch (e.updateStatus) {
|
switch (e.updateStatus) {
|
||||||
case ExchangeUpdateStatus.Finished:
|
case ExchangeUpdateStatus.Finished:
|
||||||
if (e.lastError) {
|
if (e.lastError) {
|
||||||
@ -71,30 +74,9 @@ async function gatherExchangePending(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!e.details) {
|
const details = await getExchangeDetails(tx, e.baseUrl);
|
||||||
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 keysUpdateRequired =
|
const keysUpdateRequired =
|
||||||
e.details && e.details.nextUpdateTime.t_ms < now.t_ms;
|
details && details.nextUpdateTime.t_ms < now.t_ms;
|
||||||
if (keysUpdateRequired) {
|
if (keysUpdateRequired) {
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
type: PendingOperationType.ExchangeUpdate,
|
||||||
@ -106,7 +88,7 @@ async function gatherExchangePending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
e.details &&
|
details &&
|
||||||
(!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms)
|
(!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms)
|
||||||
) {
|
) {
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
|
@ -24,9 +24,25 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* 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 { 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 { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
@ -34,6 +50,7 @@ import { TransactionHandle } from "../util/query";
|
|||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
||||||
import { getReserveRequestTimeout, processReserve } from "./reserves";
|
import { getReserveRequestTimeout, processReserve } from "./reserves";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
@ -155,12 +172,13 @@ async function recoupWithdrawCoin(
|
|||||||
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
|
const exchangeDetails = await ws.db.runWithReadTransaction(
|
||||||
if (!exchange) {
|
[Stores.exchanges, Stores.exchangeDetails],
|
||||||
// FIXME: report inconsistency?
|
async (tx) => {
|
||||||
return;
|
return getExchangeDetails(tx, reserve.exchangeBaseUrl);
|
||||||
}
|
},
|
||||||
const exchangeDetails = exchange.details;
|
);
|
||||||
|
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
// FIXME: report inconsistency?
|
// FIXME: report inconsistency?
|
||||||
return;
|
return;
|
||||||
@ -232,13 +250,14 @@ async function recoupRefreshCoin(
|
|||||||
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
|
const exchangeDetails = await ws.db.runWithReadTransaction(
|
||||||
if (!exchange) {
|
[Stores.exchanges, Stores.exchangeDetails],
|
||||||
logger.warn("exchange for recoup does not exist anymore");
|
async (tx) => {
|
||||||
// FIXME: report inconsistency?
|
// FIXME: Get the exchange details based on the
|
||||||
return;
|
// exchange master public key instead of via just the URL.
|
||||||
}
|
return getExchangeDetails(tx, coin.exchangeBaseUrl);
|
||||||
const exchangeDetails = exchange.details;
|
},
|
||||||
|
);
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
// FIXME: report inconsistency?
|
// FIXME: report inconsistency?
|
||||||
logger.warn("exchange details for recoup not found");
|
logger.warn("exchange details for recoup not found");
|
||||||
|
@ -122,7 +122,7 @@ async function refreshCreateSession(
|
|||||||
throw Error("Can't refresh, coin not found");
|
throw Error("Can't refresh, coin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
|
const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
|
||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
throw Error("db inconsistent: exchange of coin not found");
|
throw Error("db inconsistent: exchange of coin not found");
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,11 @@ import {
|
|||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import { guardOperationException, OperationFailedError } from "./errors.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 { InternalWalletState } from "./state.js";
|
||||||
import {
|
import {
|
||||||
updateWithdrawalDenoms,
|
updateWithdrawalDenoms,
|
||||||
@ -148,12 +152,15 @@ export async function createReserve(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
||||||
const exchangeDetails = exchangeInfo.details;
|
const exchangeDetails = exchangeInfo.exchangeDetails;
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
logger.trace(exchangeDetails);
|
logger.trace(exchangeDetails);
|
||||||
throw Error("exchange not updated");
|
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(
|
const resp = await ws.db.runWithWriteTransaction(
|
||||||
[Stores.exchangeTrustStore, Stores.reserves, Stores.bankWithdrawUris],
|
[Stores.exchangeTrustStore, Stores.reserves, Stores.bankWithdrawUris],
|
||||||
@ -728,7 +735,11 @@ export async function createTalerWithdrawReserve(
|
|||||||
* Get payto URIs needed to fund a reserve.
|
* Get payto URIs needed to fund a reserve.
|
||||||
*/
|
*/
|
||||||
export async function getFundingPaytoUris(
|
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,
|
reservePub: string,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const r = await tx.get(Stores.reserves, reservePub);
|
const r = await tx.get(Stores.reserves, reservePub);
|
||||||
@ -736,13 +747,13 @@ export async function getFundingPaytoUris(
|
|||||||
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
|
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
|
const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
|
||||||
if (!exchange) {
|
if (!exchangeDetails) {
|
||||||
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
|
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const plainPaytoUris =
|
const plainPaytoUris =
|
||||||
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
|
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
|
||||||
if (!plainPaytoUris) {
|
if (!plainPaytoUris) {
|
||||||
logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
|
logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
|
||||||
return [];
|
return [];
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
OrderShortInfo,
|
OrderShortInfo,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { getFundingPaytoUris } from "./reserves";
|
import { getFundingPaytoUris } from "./reserves";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an event ID from the type and the primary key for the event.
|
* 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.coins,
|
||||||
Stores.denominations,
|
Stores.denominations,
|
||||||
Stores.exchanges,
|
Stores.exchanges,
|
||||||
|
Stores.exchangeDetails,
|
||||||
Stores.proposals,
|
Stores.proposals,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
Stores.refreshGroups,
|
Stores.refreshGroups,
|
||||||
@ -134,15 +136,18 @@ export async function getTransactions(
|
|||||||
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
|
const exchangeDetails = await getExchangeDetails(
|
||||||
if (!exchange) {
|
tx,
|
||||||
|
wsr.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
if (!exchangeDetails) {
|
||||||
// FIXME: report somehow
|
// FIXME: report somehow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
withdrawalDetails = {
|
withdrawalDetails = {
|
||||||
type: WithdrawalType.ManualTransfer,
|
type: WithdrawalType.ManualTransfer,
|
||||||
exchangePaytoUris:
|
exchangePaytoUris:
|
||||||
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
|
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
transactions.push({
|
transactions.push({
|
||||||
|
@ -35,7 +35,7 @@ import {
|
|||||||
PlanchetRecord,
|
PlanchetRecord,
|
||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeWireInfo,
|
ExchangeDetailsRecord,
|
||||||
} from "../db";
|
} from "../db";
|
||||||
import {
|
import {
|
||||||
BankWithdrawDetails,
|
BankWithdrawDetails,
|
||||||
@ -51,7 +51,7 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { getExchangeDetails, updateExchangeFromUrl } from "./exchanges";
|
||||||
import {
|
import {
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
@ -94,6 +94,8 @@ interface ExchangeWithdrawDetails {
|
|||||||
*/
|
*/
|
||||||
exchangeInfo: ExchangeRecord;
|
exchangeInfo: ExchangeRecord;
|
||||||
|
|
||||||
|
exchangeDetails: ExchangeDetailsRecord;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filtered wire info to send to the bank.
|
* Filtered wire info to send to the bank.
|
||||||
*/
|
*/
|
||||||
@ -114,11 +116,6 @@ interface ExchangeWithdrawDetails {
|
|||||||
*/
|
*/
|
||||||
overhead: AmountJson;
|
overhead: AmountJson;
|
||||||
|
|
||||||
/**
|
|
||||||
* Wire fees from the exchange.
|
|
||||||
*/
|
|
||||||
wireFees: ExchangeWireInfo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the wallet know about an auditor for
|
* Does the wallet know about an auditor for
|
||||||
* the exchange that the reserve.
|
* the exchange that the reserve.
|
||||||
@ -639,12 +636,12 @@ export async function updateWithdrawalDenoms(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
const exchangeDetails = await ws.db.runWithReadTransaction(
|
||||||
if (!exchange) {
|
[Stores.exchanges, Stores.exchangeDetails],
|
||||||
logger.error("exchange not found");
|
async (tx) => {
|
||||||
throw Error(`exchange ${exchangeBaseUrl} not found`);
|
return getExchangeDetails(tx, exchangeBaseUrl);
|
||||||
}
|
},
|
||||||
const exchangeDetails = exchange.details;
|
);
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
logger.error("exchange details not available");
|
logger.error("exchange details not available");
|
||||||
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
||||||
@ -849,25 +846,19 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
amount: AmountJson,
|
amount: AmountJson,
|
||||||
): Promise<ExchangeWithdrawDetails> {
|
): Promise<ExchangeWithdrawDetails> {
|
||||||
const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl);
|
const { exchange, exchangeDetails } = await updateExchangeFromUrl(
|
||||||
const exchangeDetails = exchangeInfo.details;
|
ws,
|
||||||
if (!exchangeDetails) {
|
baseUrl,
|
||||||
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
|
);
|
||||||
}
|
|
||||||
const exchangeWireInfo = exchangeInfo.wireInfo;
|
|
||||||
if (!exchangeWireInfo) {
|
|
||||||
throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateWithdrawalDenoms(ws, baseUrl);
|
await updateWithdrawalDenoms(ws, baseUrl);
|
||||||
const denoms = await getCandidateWithdrawalDenoms(ws, baseUrl);
|
const denoms = await getCandidateWithdrawalDenoms(ws, baseUrl);
|
||||||
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
|
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
|
||||||
const exchangeWireAccounts: string[] = [];
|
const exchangeWireAccounts: string[] = [];
|
||||||
for (const account of exchangeWireInfo.accounts) {
|
for (const account of exchangeDetails.wireInfo.accounts) {
|
||||||
exchangeWireAccounts.push(account.payto_uri);
|
exchangeWireAccounts.push(account.payto_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
|
const { isTrusted, isAudited } = await getExchangeTrust(ws, exchange);
|
||||||
|
|
||||||
let earliestDepositExpiration =
|
let earliestDepositExpiration =
|
||||||
selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
|
selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
|
||||||
@ -904,10 +895,10 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
|
|
||||||
let tosAccepted = false;
|
let tosAccepted = false;
|
||||||
|
|
||||||
if (exchangeInfo.termsOfServiceLastEtag) {
|
if (exchangeDetails.termsOfServiceLastEtag) {
|
||||||
if (
|
if (
|
||||||
exchangeInfo.termsOfServiceAcceptedEtag ===
|
exchangeDetails.termsOfServiceAcceptedEtag ===
|
||||||
exchangeInfo.termsOfServiceLastEtag
|
exchangeDetails.termsOfServiceLastEtag
|
||||||
) {
|
) {
|
||||||
tosAccepted = true;
|
tosAccepted = true;
|
||||||
}
|
}
|
||||||
@ -920,7 +911,8 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
|
|
||||||
const ret: ExchangeWithdrawDetails = {
|
const ret: ExchangeWithdrawDetails = {
|
||||||
earliestDepositExpiration,
|
earliestDepositExpiration,
|
||||||
exchangeInfo,
|
exchangeInfo: exchange,
|
||||||
|
exchangeDetails,
|
||||||
exchangeWireAccounts,
|
exchangeWireAccounts,
|
||||||
exchangeVersion: exchangeDetails.protocolVersion || "unknown",
|
exchangeVersion: exchangeDetails.protocolVersion || "unknown",
|
||||||
isAudited,
|
isAudited,
|
||||||
@ -932,7 +924,6 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
trustedAuditorPubs: [],
|
trustedAuditorPubs: [],
|
||||||
versionMatch,
|
versionMatch,
|
||||||
walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
wireFees: exchangeWireInfo,
|
|
||||||
withdrawFee,
|
withdrawFee,
|
||||||
termsOfServiceAccepted: tosAccepted,
|
termsOfServiceAccepted: tosAccepted,
|
||||||
};
|
};
|
||||||
@ -960,29 +951,25 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchangesRes: (ExchangeListItem | undefined)[] = await ws.db
|
const exchanges: ExchangeListItem[] = [];
|
||||||
.iter(Stores.exchanges)
|
|
||||||
.map((x) => {
|
const exchangeRecords = await ws.db.iter(Stores.exchanges).toArray();
|
||||||
const details = x.details;
|
|
||||||
if (!details) {
|
for (const r of exchangeRecords) {
|
||||||
return undefined;
|
const details = await ws.db.runWithReadTransaction(
|
||||||
}
|
[Stores.exchanges, Stores.exchangeDetails],
|
||||||
if (!x.addComplete) {
|
async (tx) => {
|
||||||
return undefined;
|
return getExchangeDetails(tx, r.baseUrl);
|
||||||
}
|
},
|
||||||
if (!x.wireInfo) {
|
);
|
||||||
return undefined;
|
if (details) {
|
||||||
}
|
exchanges.push({
|
||||||
if (details.currency !== info.amount.currency) {
|
exchangeBaseUrl: details.exchangeBaseUrl,
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
exchangeBaseUrl: x.baseUrl,
|
|
||||||
currency: details.currency,
|
currency: details.currency,
|
||||||
paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
|
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
|
||||||
};
|
});
|
||||||
});
|
}
|
||||||
const exchanges = exchangesRes.filter((x) => !!x) as ExchangeListItem[];
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amount: Amounts.stringify(info.amount),
|
amount: Amounts.stringify(info.amount),
|
||||||
|
@ -105,6 +105,7 @@ import {
|
|||||||
CoinRecord,
|
CoinRecord,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
|
ExchangeDetailsRecord,
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
RefundState,
|
RefundState,
|
||||||
@ -232,7 +233,7 @@ export class Wallet {
|
|||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
amount,
|
amount,
|
||||||
);
|
);
|
||||||
const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
|
const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
|
||||||
(x) => x.payto_uri,
|
(x) => x.payto_uri,
|
||||||
);
|
);
|
||||||
if (!paytoUris) {
|
if (!paytoUris) {
|
||||||
@ -586,13 +587,14 @@ export class Wallet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update or add exchange DB entry by fetching the /keys and /wire information.
|
* 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(
|
async updateExchangeFromUrl(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
force = false,
|
force = false,
|
||||||
): Promise<ExchangeRecord> {
|
): Promise<{
|
||||||
|
exchange: ExchangeRecord;
|
||||||
|
exchangeDetails: ExchangeDetailsRecord;
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
return updateExchangeFromUrl(this.ws, baseUrl, force);
|
return updateExchangeFromUrl(this.ws, baseUrl, force);
|
||||||
} finally {
|
} finally {
|
||||||
@ -601,14 +603,16 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> {
|
async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> {
|
||||||
const exchange = await this.updateExchangeFromUrl(exchangeBaseUrl);
|
const { exchange, exchangeDetails } = await this.updateExchangeFromUrl(
|
||||||
const tos = exchange.termsOfServiceText;
|
exchangeBaseUrl,
|
||||||
const currentEtag = exchange.termsOfServiceLastEtag;
|
);
|
||||||
|
const tos = exchangeDetails.termsOfServiceText;
|
||||||
|
const currentEtag = exchangeDetails.termsOfServiceLastEtag;
|
||||||
if (!tos || !currentEtag) {
|
if (!tos || !currentEtag) {
|
||||||
throw Error("exchange is in invalid state");
|
throw Error("exchange is in invalid state");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
acceptedEtag: exchange.termsOfServiceAcceptedEtag,
|
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
|
||||||
currentEtag,
|
currentEtag,
|
||||||
tos,
|
tos,
|
||||||
};
|
};
|
||||||
@ -678,28 +682,29 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getExchanges(): Promise<ExchangesListRespose> {
|
async getExchanges(): Promise<ExchangesListRespose> {
|
||||||
const exchanges: (ExchangeListItem | undefined)[] = await this.db
|
const exchangeRecords = await this.db.iter(Stores.exchanges).toArray();
|
||||||
.iter(Stores.exchanges)
|
const exchanges: ExchangeListItem[] = [];
|
||||||
.map((x) => {
|
for (const r of exchangeRecords) {
|
||||||
const details = x.details;
|
const dp = r.detailsPointer;
|
||||||
if (!details) {
|
if (!dp) {
|
||||||
return undefined;
|
continue;
|
||||||
}
|
}
|
||||||
if (!x.addComplete) {
|
const { currency, masterPublicKey } = dp;
|
||||||
return undefined;
|
const exchangeDetails = await this.db.get(Stores.exchangeDetails, [
|
||||||
}
|
r.baseUrl,
|
||||||
if (!x.wireInfo) {
|
currency,
|
||||||
return undefined;
|
masterPublicKey,
|
||||||
}
|
]);
|
||||||
return {
|
if (!exchangeDetails) {
|
||||||
exchangeBaseUrl: x.baseUrl,
|
continue;
|
||||||
currency: details.currency,
|
}
|
||||||
paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
|
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> {
|
async getCurrencies(): Promise<WalletCurrencyInfo> {
|
||||||
|
Loading…
Reference in New Issue
Block a user