aboutsummaryrefslogtreecommitdiff
path: root/src/wallet-impl
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet-impl')
-rw-r--r--src/wallet-impl/balance.ts158
-rw-r--r--src/wallet-impl/errors.ts84
-rw-r--r--src/wallet-impl/exchanges.ts505
-rw-r--r--src/wallet-impl/history.ts221
-rw-r--r--src/wallet-impl/pay.ts1494
-rw-r--r--src/wallet-impl/payback.ts93
-rw-r--r--src/wallet-impl/pending.ts452
-rw-r--r--src/wallet-impl/refresh.ts479
-rw-r--r--src/wallet-impl/reserves.ts630
-rw-r--r--src/wallet-impl/return.ts271
-rw-r--r--src/wallet-impl/state.ts68
-rw-r--r--src/wallet-impl/tip.ts304
-rw-r--r--src/wallet-impl/withdraw.ts699
13 files changed, 0 insertions, 5458 deletions
diff --git a/src/wallet-impl/balance.ts b/src/wallet-impl/balance.ts
deleted file mode 100644
index 8ce91a173..000000000
--- a/src/wallet-impl/balance.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { WalletBalance, WalletBalanceEntry } from "../walletTypes";
-import { runWithReadTransaction } from "../util/query";
-import { InternalWalletState } from "./state";
-import { Stores, TipRecord, CoinStatus } from "../dbTypes";
-import * as Amounts from "../util/amounts";
-import { AmountJson } from "../util/amounts";
-import { Logger } from "../util/logging";
-
-const logger = new Logger("withdraw.ts");
-
-/**
- * Get detailed balance information, sliced by exchange and by currency.
- */
-export async function getBalances(
- ws: InternalWalletState,
-): Promise<WalletBalance> {
- logger.trace("starting to compute balance");
- /**
- * Add amount to a balance field, both for
- * the slicing by exchange and currency.
- */
- function addTo(
- balance: WalletBalance,
- field: keyof WalletBalanceEntry,
- amount: AmountJson,
- exchange: string,
- ): void {
- const z = Amounts.getZero(amount.currency);
- const balanceIdentity = {
- available: z,
- paybackAmount: z,
- pendingIncoming: z,
- pendingPayment: z,
- pendingIncomingDirty: z,
- pendingIncomingRefresh: z,
- pendingIncomingWithdraw: z,
- };
- let entryCurr = balance.byCurrency[amount.currency];
- if (!entryCurr) {
- balance.byCurrency[amount.currency] = entryCurr = {
- ...balanceIdentity,
- };
- }
- let entryEx = balance.byExchange[exchange];
- if (!entryEx) {
- balance.byExchange[exchange] = entryEx = { ...balanceIdentity };
- }
- entryCurr[field] = Amounts.add(entryCurr[field], amount).amount;
- entryEx[field] = Amounts.add(entryEx[field], amount).amount;
- }
-
- const balanceStore = {
- byCurrency: {},
- byExchange: {},
- };
-
- await runWithReadTransaction(
- ws.db,
- [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases, Stores.withdrawalSession],
- async tx => {
- await tx.iter(Stores.coins).forEach(c => {
- if (c.suspended) {
- return;
- }
- if (c.status === CoinStatus.Fresh) {
- addTo(balanceStore, "available", c.currentAmount, c.exchangeBaseUrl);
- }
- if (c.status === CoinStatus.Dirty) {
- addTo(
- balanceStore,
- "pendingIncoming",
- c.currentAmount,
- c.exchangeBaseUrl,
- );
- addTo(
- balanceStore,
- "pendingIncomingDirty",
- c.currentAmount,
- c.exchangeBaseUrl,
- );
- }
- });
- await tx.iter(Stores.refresh).forEach(r => {
- // Don't count finished refreshes, since the refresh already resulted
- // in coins being added to the wallet.
- if (r.finishedTimestamp) {
- return;
- }
- addTo(
- balanceStore,
- "pendingIncoming",
- r.valueOutput,
- r.exchangeBaseUrl,
- );
- addTo(
- balanceStore,
- "pendingIncomingRefresh",
- r.valueOutput,
- r.exchangeBaseUrl,
- );
- });
-
- await tx.iter(Stores.withdrawalSession).forEach(wds => {
- let w = wds.totalCoinValue;
- for (let i = 0; i < wds.planchets.length; i++) {
- if (wds.withdrawn[i]) {
- const p = wds.planchets[i];
- if (p) {
- w = Amounts.sub(w, p.coinValue).amount;
- }
- }
- }
- addTo(
- balanceStore,
- "pendingIncoming",
- w,
- wds.exchangeBaseUrl,
- );
- });
-
- await tx.iter(Stores.purchases).forEach(t => {
- if (t.firstSuccessfulPayTimestamp) {
- return;
- }
- for (const c of t.payReq.coins) {
- addTo(
- balanceStore,
- "pendingPayment",
- Amounts.parseOrThrow(c.contribution),
- c.exchange_url,
- );
- }
- });
- },
- );
-
- logger.trace("computed balances:", balanceStore);
- return balanceStore;
-}
diff --git a/src/wallet-impl/errors.ts b/src/wallet-impl/errors.ts
deleted file mode 100644
index 803497e66..000000000
--- a/src/wallet-impl/errors.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { OperationError } from "../walletTypes";
-
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * This exception is there to let the caller know that an error happened,
- * but the error has already been reported by writing it to the database.
- */
-export class OperationFailedAndReportedError extends Error {
- constructor(message: string) {
- super(message);
-
- // Set the prototype explicitly.
- Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
- }
-}
-
-/**
- * This exception is thrown when an error occured and the caller is
- * responsible for recording the failure in the database.
- */
-export class OperationFailedError extends Error {
- constructor(message: string, public err: OperationError) {
- super(message);
-
- // Set the prototype explicitly.
- Object.setPrototypeOf(this, OperationFailedError.prototype);
- }
-}
-
-/**
- * Run an operation and call the onOpError callback
- * when there was an exception or operation error that must be reported.
- * The cause will be re-thrown to the caller.
- */
-export async function guardOperationException<T>(
- op: () => Promise<T>,
- onOpError: (e: OperationError) => Promise<void>,
-): Promise<T> {
- try {
- return await op();
- } catch (e) {
- console.log("guard: caught exception");
- if (e instanceof OperationFailedAndReportedError) {
- throw e;
- }
- if (e instanceof OperationFailedError) {
- await onOpError(e.err);
- throw new OperationFailedAndReportedError(e.message);
- }
- if (e instanceof Error) {
- console.log("guard: caught Error");
- await onOpError({
- type: "exception",
- message: e.message,
- details: {},
- });
- throw new OperationFailedAndReportedError(e.message);
- }
- console.log("guard: caught something else");
- await onOpError({
- type: "exception",
- message: "non-error exception thrown",
- details: {
- value: e.toString(),
- },
- });
- throw new OperationFailedAndReportedError(e.message);
- }
-} \ No newline at end of file
diff --git a/src/wallet-impl/exchanges.ts b/src/wallet-impl/exchanges.ts
deleted file mode 100644
index 1e5f86b4f..000000000
--- a/src/wallet-impl/exchanges.ts
+++ /dev/null
@@ -1,505 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { InternalWalletState } from "./state";
-import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "../wallet";
-import { KeysJson, Denomination, ExchangeWireJson } from "../talerTypes";
-import { getTimestampNow, OperationError } from "../walletTypes";
-import {
- ExchangeRecord,
- ExchangeUpdateStatus,
- Stores,
- DenominationRecord,
- DenominationStatus,
- WireFee,
-} from "../dbTypes";
-import {
- canonicalizeBaseUrl,
- extractTalerStamp,
- extractTalerStampOrThrow,
-} from "../util/helpers";
-import {
- oneShotGet,
- oneShotPut,
- runWithWriteTransaction,
- oneShotMutate,
-} from "../util/query";
-import * as Amounts from "../util/amounts";
-import { parsePaytoUri } from "../util/payto";
-import {
- OperationFailedAndReportedError,
- guardOperationException,
-} from "./errors";
-
-async function denominationRecordFromKeys(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- denomIn: Denomination,
-): Promise<DenominationRecord> {
- const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub);
- const d: DenominationRecord = {
- denomPub: denomIn.denom_pub,
- denomPubHash,
- exchangeBaseUrl,
- feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
- feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
- feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
- feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
- isOffered: true,
- masterSig: denomIn.master_sig,
- stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit),
- stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
- stampExpireWithdraw: extractTalerStampOrThrow(
- denomIn.stamp_expire_withdraw,
- ),
- stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
- status: DenominationStatus.Unverified,
- value: Amounts.parseOrThrow(denomIn.value),
- };
- return d;
-}
-
-async function setExchangeError(
- ws: InternalWalletState,
- baseUrl: string,
- err: OperationError,
-): Promise<void> {
- const mut = (exchange: ExchangeRecord) => {
- exchange.lastError = err;
- return exchange;
- };
- await oneShotMutate(ws.db, Stores.exchanges, baseUrl, mut);
-}
-
-/**
- * Fetch the exchange's /keys and update our database accordingly.
- *
- * 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 oneShotGet(
- ws.db,
- Stores.exchanges,
- baseUrl,
- );
-
- if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
- return;
- }
- const keysUrl = new URL("keys", baseUrl);
- keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-
- let keysResp;
- try {
- const r = await ws.http.get(keysUrl.href);
- if (r.status !== 200) {
- throw Error(`unexpected status for keys: ${r.status}`);
- }
- keysResp = await r.json();
- } catch (e) {
- const m = `Fetching keys failed: ${e.message}`;
- await setExchangeError(ws, baseUrl, {
- type: "network",
- details: {
- requestUrl: e.config?.url,
- },
- message: m,
- });
- throw new OperationFailedAndReportedError(m);
- }
- let exchangeKeysJson: KeysJson;
- try {
- exchangeKeysJson = KeysJson.checked(keysResp);
- } catch (e) {
- const m = `Parsing /keys response failed: ${e.message}`;
- await setExchangeError(ws, baseUrl, {
- type: "protocol-violation",
- details: {},
- message: m,
- });
- throw new OperationFailedAndReportedError(m);
- }
-
- const lastUpdateTimestamp = extractTalerStamp(
- exchangeKeysJson.list_issue_date,
- );
- if (!lastUpdateTimestamp) {
- const m = `Parsing /keys response failed: invalid list_issue_date.`;
- await setExchangeError(ws, baseUrl, {
- type: "protocol-violation",
- details: {},
- message: m,
- });
- throw new OperationFailedAndReportedError(m);
- }
-
- if (exchangeKeysJson.denoms.length === 0) {
- const m = "exchange doesn't offer any denominations";
- await setExchangeError(ws, baseUrl, {
- type: "protocol-violation",
- details: {},
- message: m,
- });
- throw new OperationFailedAndReportedError(m);
- }
-
- const protocolVersion = exchangeKeysJson.version;
- if (!protocolVersion) {
- const m = "outdate exchange, no version in /keys response";
- await setExchangeError(ws, baseUrl, {
- type: "protocol-violation",
- details: {},
- message: m,
- });
- throw new OperationFailedAndReportedError(m);
- }
-
- const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
- .currency;
-
- const newDenominations = await Promise.all(
- exchangeKeysJson.denoms.map(d =>
- denominationRecordFromKeys(ws, baseUrl, d),
- ),
- );
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.exchanges, Stores.denominations],
- async tx => {
- const r = await tx.get(Stores.exchanges, baseUrl);
- if (!r) {
- console.warn(`exchange ${baseUrl} no longer present`);
- return;
- }
- if (r.details) {
- // FIXME: We need to do some consistency checks!
- }
- r.details = {
- auditors: exchangeKeysJson.auditors,
- currency: currency,
- lastUpdateTime: lastUpdateTimestamp,
- masterPublicKey: exchangeKeysJson.master_public_key,
- protocolVersion: protocolVersion,
- };
- r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
- r.lastError = undefined;
- await tx.put(Stores.exchanges, r);
-
- for (const newDenom of newDenominations) {
- const oldDenom = await tx.get(Stores.denominations, [
- baseUrl,
- newDenom.denomPub,
- ]);
- if (oldDenom) {
- // FIXME: Do consistency check
- } else {
- await tx.put(Stores.denominations, newDenom);
- }
- }
- },
- );
-}
-
-async function updateExchangeWithTermsOfService(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-) {
- const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
- 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 });
- if (resp.status !== 200) {
- throw Error(`/terms response has unexpected status code (${resp.status})`);
- }
-
- const tosText = await resp.text();
- const tosEtag = resp.headers.get("etag") || undefined;
-
- await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- if (r.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
- return;
- }
- r.termsOfServiceText = tosText;
- r.termsOfServiceLastEtag = tosEtag;
- r.updateStatus = ExchangeUpdateStatus.FINISHED;
- await tx.put(Stores.exchanges, r);
- });
-}
-
-export async function acceptExchangeTermsOfService(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- etag: string | undefined,
-) {
- await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- r.termsOfServiceAcceptedEtag = etag;
- r.termsOfServiceAcceptedTimestamp = getTimestampNow();
- 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,
-) {
- const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
- 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);
- if (resp.status !== 200) {
- throw Error(`/wire response has unexpected status code (${resp.status})`);
- }
- const wiJson = await resp.json();
- if (!wiJson) {
- throw Error("/wire response malformed");
- }
- const wireInfo = ExchangeWireJson.checked(wiJson);
- for (const a of wireInfo.accounts) {
- console.log("validating exchange acct");
- const isValid = await ws.cryptoApi.isValidWireAccount(
- a.url,
- 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 = extractTalerStamp(x.start_date);
- if (!startStamp) {
- throw Error("wrong date format");
- }
- const endStamp = extractTalerStamp(x.end_date);
- if (!endStamp) {
- throw Error("wrong date format");
- }
- 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 runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
- const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
- if (!r) {
- return;
- }
- if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
- return;
- }
- r.wireInfo = {
- accounts: wireInfo.accounts,
- feesForType: feesForType,
- };
- r.updateStatus = ExchangeUpdateStatus.FETCH_TERMS;
- r.lastError = undefined;
- await tx.put(Stores.exchanges, r);
- });
-}
-
-export async function updateExchangeFromUrl(
- ws: InternalWalletState,
- baseUrl: string,
- forceNow: boolean = false,
-): Promise<ExchangeRecord> {
- const onOpErr = (e: OperationError) => setExchangeError(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: boolean = false,
-): Promise<ExchangeRecord> {
- const now = getTimestampNow();
- baseUrl = canonicalizeBaseUrl(baseUrl);
-
- const r = await oneShotGet(ws.db, Stores.exchanges, baseUrl);
- if (!r) {
- const newExchangeRecord: ExchangeRecord = {
- baseUrl: baseUrl,
- details: undefined,
- wireInfo: undefined,
- updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
- updateStarted: now,
- updateReason: "initial",
- timestampAdded: getTimestampNow(),
- termsOfServiceAcceptedEtag: undefined,
- termsOfServiceAcceptedTimestamp: undefined,
- termsOfServiceLastEtag: undefined,
- termsOfServiceText: undefined,
- };
- await oneShotPut(ws.db, Stores.exchanges, newExchangeRecord);
- } else {
- await runWithWriteTransaction(ws.db, [Stores.exchanges], async t => {
- const rec = await t.get(Stores.exchanges, baseUrl);
- if (!rec) {
- return;
- }
- if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !forceNow) {
- return;
- }
- if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && forceNow) {
- rec.updateReason = "forced";
- }
- rec.updateStarted = now;
- rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
- rec.lastError = undefined;
- t.put(Stores.exchanges, rec);
- });
- }
-
- await updateExchangeWithKeys(ws, baseUrl);
- await updateExchangeWithWireInfo(ws, baseUrl);
- await updateExchangeWithTermsOfService(ws, baseUrl);
-
- const updatedExchange = await oneShotGet(ws.db, Stores.exchanges, baseUrl);
-
- if (!updatedExchange) {
- // This should practically never happen
- throw Error("exchange not found");
- }
- return updatedExchange;
-}
-
-/**
- * Check if and how an exchange is trusted and/or audited.
- */
-export async function getExchangeTrust(
- ws: InternalWalletState,
- exchangeInfo: ExchangeRecord,
-): Promise<{ isTrusted: boolean; isAudited: boolean }> {
- let isTrusted = false;
- let isAudited = false;
- const exchangeDetails = exchangeInfo.details;
- if (!exchangeDetails) {
- throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
- }
- const currencyRecord = await oneShotGet(
- ws.db,
- Stores.currencies,
- exchangeDetails.currency,
- );
- if (currencyRecord) {
- for (const trustedExchange of currencyRecord.exchanges) {
- if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
- isTrusted = true;
- break;
- }
- }
- for (const trustedAuditor of currencyRecord.auditors) {
- for (const exchangeAuditor of exchangeDetails.auditors) {
- if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
- isAudited = true;
- break;
- }
- }
- }
- }
- return { isTrusted, isAudited };
-}
-
-export async function getExchangePaytoUri(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- supportedTargetTypes: string[],
-): 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 (let account of exchangeWireInfo.accounts) {
- const res = parsePaytoUri(account.url);
- if (!res) {
- continue;
- }
- if (supportedTargetTypes.includes(res.targetType)) {
- return account.url;
- }
- }
- throw Error("no matching exchange account found");
-}
diff --git a/src/wallet-impl/history.ts b/src/wallet-impl/history.ts
deleted file mode 100644
index 99e51c8de..000000000
--- a/src/wallet-impl/history.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { HistoryQuery, HistoryEvent } from "../walletTypes";
-import { oneShotIter, runWithReadTransaction } from "../util/query";
-import { InternalWalletState } from "./state";
-import { Stores, TipRecord } from "../dbTypes";
-import * as Amounts from "../util/amounts";
-import { AmountJson } from "../util/amounts";
-
-/**
- * Retrive the full event history for this wallet.
- */
-export async function getHistory(
- ws: InternalWalletState,
- historyQuery?: HistoryQuery,
-): Promise<{ history: HistoryEvent[] }> {
- const history: HistoryEvent[] = [];
-
- // FIXME: do pagination instead of generating the full history
- // We uniquely identify history rows via their timestamp.
- // This works as timestamps are guaranteed to be monotonically
- // increasing even
-
- await runWithReadTransaction(
- ws.db,
- [
- Stores.currencies,
- Stores.coins,
- Stores.denominations,
- Stores.exchanges,
- Stores.proposals,
- Stores.purchases,
- Stores.refresh,
- Stores.reserves,
- Stores.tips,
- Stores.withdrawalSession,
- ],
- async tx => {
- await tx.iter(Stores.proposals).forEach(p => {
- history.push({
- detail: {},
- timestamp: p.timestamp,
- type: "claim-order",
- explicit: false,
- });
- });
-
- await tx.iter(Stores.withdrawalSession).forEach(w => {
- history.push({
- detail: {
- withdrawalAmount: w.rawWithdrawalAmount,
- },
- timestamp: w.startTimestamp,
- type: "withdraw-started",
- explicit: false,
- });
- if (w.finishTimestamp) {
- history.push({
- detail: {
- withdrawalAmount: w.rawWithdrawalAmount,
- },
- timestamp: w.finishTimestamp,
- type: "withdraw-finished",
- explicit: false,
- });
- }
- });
-
- await tx.iter(Stores.purchases).forEach(p => {
- history.push({
- detail: {
- amount: p.contractTerms.amount,
- contractTermsHash: p.contractTermsHash,
- fulfillmentUrl: p.contractTerms.fulfillment_url,
- merchantName: p.contractTerms.merchant.name,
- },
- timestamp: p.acceptTimestamp,
- type: "pay-started",
- explicit: false,
- });
- if (p.firstSuccessfulPayTimestamp) {
- history.push({
- detail: {
- amount: p.contractTerms.amount,
- contractTermsHash: p.contractTermsHash,
- fulfillmentUrl: p.contractTerms.fulfillment_url,
- merchantName: p.contractTerms.merchant.name,
- },
- timestamp: p.firstSuccessfulPayTimestamp,
- type: "pay-finished",
- explicit: false,
- });
- }
- if (p.lastRefundStatusTimestamp) {
- const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
- const amountsPending = Object.keys(p.refundsPending).map(x =>
- Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
- );
- const amountsDone = Object.keys(p.refundsDone).map(x =>
- Amounts.parseOrThrow(p.refundsDone[x].refund_amount),
- );
- const amounts: AmountJson[] = amountsPending.concat(amountsDone);
- const amount = Amounts.add(
- Amounts.getZero(contractAmount.currency),
- ...amounts,
- ).amount;
-
- history.push({
- detail: {
- contractTermsHash: p.contractTermsHash,
- fulfillmentUrl: p.contractTerms.fulfillment_url,
- merchantName: p.contractTerms.merchant.name,
- refundAmount: amount,
- },
- timestamp: p.lastRefundStatusTimestamp,
- type: "refund",
- explicit: false,
- });
- }
- });
-
- await tx.iter(Stores.reserves).forEach(r => {
- const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
- history.push({
- detail: {
- exchangeBaseUrl: r.exchangeBaseUrl,
- requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
- reservePub: r.reservePub,
- reserveType,
- bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
- },
- timestamp: r.created,
- type: "reserve-created",
- explicit: false,
- });
- if (r.timestampConfirmed) {
- history.push({
- detail: {
- exchangeBaseUrl: r.exchangeBaseUrl,
- requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
- reservePub: r.reservePub,
- reserveType,
- bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
- },
- timestamp: r.created,
- type: "reserve-confirmed",
- explicit: false,
- });
- }
- });
-
- await tx.iter(Stores.tips).forEach(tip => {
- history.push({
- detail: {
- accepted: tip.accepted,
- amount: tip.amount,
- merchantBaseUrl: tip.merchantBaseUrl,
- tipId: tip.merchantTipId,
- },
- timestamp: tip.createdTimestamp,
- explicit: false,
- type: "tip",
- });
- });
-
- await tx.iter(Stores.exchanges).forEach(exchange => {
- history.push({
- type: "exchange-added",
- explicit: false,
- timestamp: exchange.timestampAdded,
- detail: {
- exchangeBaseUrl: exchange.baseUrl,
- },
- });
- });
-
- await tx.iter(Stores.refresh).forEach((r) => {
- history.push({
- type: "refresh-started",
- explicit: false,
- timestamp: r.created,
- detail: {
- refreshSessionId: r.refreshSessionId,
- },
- });
- if (r.finishedTimestamp) {
- history.push({
- type: "refresh-finished",
- explicit: false,
- timestamp: r.finishedTimestamp,
- detail: {
- refreshSessionId: r.refreshSessionId,
- },
- });
- }
-
- });
- },
- );
-
- history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
-
- return { history };
-}
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
deleted file mode 100644
index af9d44066..000000000
--- a/src/wallet-impl/pay.ts
+++ /dev/null
@@ -1,1494 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { AmountJson } from "../util/amounts";
-import {
- Auditor,
- ExchangeHandle,
- MerchantRefundResponse,
- PayReq,
- Proposal,
- ContractTerms,
- MerchantRefundPermission,
- RefundRequest,
-} from "../talerTypes";
-import {
- Timestamp,
- CoinSelectionResult,
- CoinWithDenom,
- PayCoinInfo,
- getTimestampNow,
- PreparePayResult,
- ConfirmPayResult,
- OperationError,
- NotificationType,
-} from "../walletTypes";
-import {
- oneShotIter,
- oneShotIterIndex,
- oneShotGet,
- runWithWriteTransaction,
- oneShotPut,
- oneShotGetIndexed,
- oneShotMutate,
-} from "../util/query";
-import {
- Stores,
- CoinStatus,
- DenominationRecord,
- ProposalRecord,
- PurchaseRecord,
- CoinRecord,
- ProposalStatus,
- initRetryInfo,
- updateRetryInfoTimeout,
-} from "../dbTypes";
-import * as Amounts from "../util/amounts";
-import {
- amountToPretty,
- strcmp,
- canonicalJson,
- extractTalerStampOrThrow,
- extractTalerDurationOrThrow,
- extractTalerDuration,
-} from "../util/helpers";
-import { Logger } from "../util/logging";
-import { InternalWalletState } from "./state";
-import {
- parsePayUri,
- parseRefundUri,
- getOrderDownloadUrl,
-} from "../util/taleruri";
-import { getTotalRefreshCost, refresh } from "./refresh";
-import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
-import { guardOperationException } from "./errors";
-import { assertUnreachable } from "../util/assertUnreachable";
-
-export interface SpeculativePayData {
- payCoinInfo: PayCoinInfo;
- exchangeUrl: string;
- orderDownloadId: string;
- proposal: ProposalRecord;
-}
-
-interface CoinsForPaymentArgs {
- allowedAuditors: Auditor[];
- allowedExchanges: ExchangeHandle[];
- depositFeeLimit: AmountJson;
- paymentAmount: AmountJson;
- wireFeeAmortization: number;
- wireFeeLimit: AmountJson;
- wireFeeTime: Timestamp;
- wireMethod: string;
-}
-
-interface SelectPayCoinsResult {
- cds: CoinWithDenom[];
- totalFees: AmountJson;
-}
-
-const logger = new Logger("pay.ts");
-
-/**
- * Select coins for a payment under the merchant's constraints.
- *
- * @param denoms all available denoms, used to compute refresh fees
- */
-export function selectPayCoins(
- denoms: DenominationRecord[],
- cds: CoinWithDenom[],
- paymentAmount: AmountJson,
- depositFeeLimit: AmountJson,
-): SelectPayCoinsResult | undefined {
- if (cds.length === 0) {
- return undefined;
- }
- // Sort by ascending deposit fee and denomPub if deposit fee is the same
- // (to guarantee deterministic results)
- cds.sort(
- (o1, o2) =>
- Amounts.cmp(o1.denom.feeDeposit, o2.denom.feeDeposit) ||
- strcmp(o1.denom.denomPub, o2.denom.denomPub),
- );
- const currency = cds[0].denom.value.currency;
- const cdsResult: CoinWithDenom[] = [];
- let accDepositFee: AmountJson = Amounts.getZero(currency);
- let accAmount: AmountJson = Amounts.getZero(currency);
- for (const { coin, denom } of cds) {
- if (coin.suspended) {
- continue;
- }
- if (coin.status !== CoinStatus.Fresh) {
- continue;
- }
- if (Amounts.cmp(denom.feeDeposit, coin.currentAmount) >= 0) {
- continue;
- }
- cdsResult.push({ coin, denom });
- accDepositFee = Amounts.add(denom.feeDeposit, accDepositFee).amount;
- let leftAmount = Amounts.sub(
- coin.currentAmount,
- Amounts.sub(paymentAmount, accAmount).amount,
- ).amount;
- accAmount = Amounts.add(coin.currentAmount, accAmount).amount;
- const coversAmount = Amounts.cmp(accAmount, paymentAmount) >= 0;
- const coversAmountWithFee =
- Amounts.cmp(
- accAmount,
- Amounts.add(paymentAmount, denom.feeDeposit).amount,
- ) >= 0;
- const isBelowFee = Amounts.cmp(accDepositFee, depositFeeLimit) <= 0;
-
- logger.trace("candidate coin selection", {
- coversAmount,
- isBelowFee,
- accDepositFee,
- accAmount,
- paymentAmount,
- });
-
- if ((coversAmount && isBelowFee) || coversAmountWithFee) {
- const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
- .amount;
- leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount;
- logger.trace("deposit fee to cover", amountToPretty(depositFeeToCover));
- let totalFees: AmountJson = Amounts.getZero(currency);
- if (coversAmountWithFee && !isBelowFee) {
- // these are the fees the customer has to pay
- // because the merchant doesn't cover them
- totalFees = Amounts.sub(depositFeeLimit, accDepositFee).amount;
- }
- totalFees = Amounts.add(
- totalFees,
- getTotalRefreshCost(denoms, denom, leftAmount),
- ).amount;
- return { cds: cdsResult, totalFees };
- }
- }
- return undefined;
-}
-
-/**
- * Get exchanges and associated coins that are still spendable, but only
- * if the sum the coins' remaining value covers the payment amount and fees.
- */
-async function getCoinsForPayment(
- ws: InternalWalletState,
- args: CoinsForPaymentArgs,
-): Promise<CoinSelectionResult | undefined> {
- const {
- allowedAuditors,
- allowedExchanges,
- depositFeeLimit,
- paymentAmount,
- wireFeeAmortization,
- wireFeeLimit,
- wireFeeTime,
- wireMethod,
- } = args;
-
- let remainingAmount = paymentAmount;
-
- const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
-
- for (const exchange of exchanges) {
- let isOkay: boolean = false;
- const exchangeDetails = exchange.details;
- if (!exchangeDetails) {
- continue;
- }
- const exchangeFees = exchange.wireInfo;
- if (!exchangeFees) {
- continue;
- }
-
- // is the exchange explicitly allowed?
- for (const allowedExchange of allowedExchanges) {
- if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
- isOkay = true;
- break;
- }
- }
-
- // is the exchange allowed because of one of its auditors?
- if (!isOkay) {
- for (const allowedAuditor of allowedAuditors) {
- for (const auditor of exchangeDetails.auditors) {
- if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
- isOkay = true;
- break;
- }
- }
- if (isOkay) {
- break;
- }
- }
- }
-
- if (!isOkay) {
- continue;
- }
-
- const coins = await oneShotIterIndex(
- ws.db,
- Stores.coins.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
-
- const denoms = await oneShotIterIndex(
- ws.db,
- Stores.denominations.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
-
- if (!coins || coins.length === 0) {
- continue;
- }
-
- // Denomination of the first coin, we assume that all other
- // coins have the same currency
- const firstDenom = await oneShotGet(ws.db, Stores.denominations, [
- exchange.baseUrl,
- coins[0].denomPub,
- ]);
- if (!firstDenom) {
- throw Error("db inconsistent");
- }
- const currency = firstDenom.value.currency;
- const cds: CoinWithDenom[] = [];
- for (const coin of coins) {
- const denom = await oneShotGet(ws.db, Stores.denominations, [
- exchange.baseUrl,
- coin.denomPub,
- ]);
- if (!denom) {
- throw Error("db inconsistent");
- }
- if (denom.value.currency !== currency) {
- console.warn(
- `same pubkey for different currencies at exchange ${exchange.baseUrl}`,
- );
- continue;
- }
- if (coin.suspended) {
- continue;
- }
- if (coin.status !== CoinStatus.Fresh) {
- continue;
- }
- cds.push({ coin, denom });
- }
-
- let totalFees = Amounts.getZero(currency);
- let wireFee: AmountJson | undefined;
- for (const fee of exchangeFees.feesForType[wireMethod] || []) {
- if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
- wireFee = fee.wireFee;
- break;
- }
- }
-
- if (wireFee) {
- const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
- if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) {
- totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
- remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount;
- }
- }
-
- const res = selectPayCoins(denoms, cds, remainingAmount, depositFeeLimit);
-
- if (res) {
- totalFees = Amounts.add(totalFees, res.totalFees).amount;
- return {
- cds: res.cds,
- exchangeUrl: exchange.baseUrl,
- totalAmount: remainingAmount,
- totalFees,
- };
- }
- }
- return undefined;
-}
-
-/**
- * Record all information that is necessary to
- * pay for a proposal in the wallet's database.
- */
-async function recordConfirmPay(
- ws: InternalWalletState,
- proposal: ProposalRecord,
- payCoinInfo: PayCoinInfo,
- chosenExchange: string,
- sessionIdOverride: string | undefined,
-): Promise<PurchaseRecord> {
- const d = proposal.download;
- if (!d) {
- throw Error("proposal is in invalid state");
- }
- let sessionId;
- if (sessionIdOverride) {
- sessionId = sessionIdOverride;
- } else {
- sessionId = proposal.downloadSessionId;
- }
- logger.trace(`recording payment with session ID ${sessionId}`);
- const payReq: PayReq = {
- coins: payCoinInfo.sigs,
- merchant_pub: d.contractTerms.merchant_pub,
- mode: "pay",
- order_id: d.contractTerms.order_id,
- };
- const t: PurchaseRecord = {
- abortDone: false,
- abortRequested: false,
- contractTerms: d.contractTerms,
- contractTermsHash: d.contractTermsHash,
- lastSessionId: sessionId,
- merchantSig: d.merchantSig,
- payReq,
- refundsDone: {},
- refundsPending: {},
- acceptTimestamp: getTimestampNow(),
- lastRefundStatusTimestamp: undefined,
- proposalId: proposal.proposalId,
- lastPayError: undefined,
- lastRefundStatusError: undefined,
- payRetryInfo: initRetryInfo(),
- refundStatusRetryInfo: initRetryInfo(),
- refundStatusRequested: false,
- lastRefundApplyError: undefined,
- refundApplyRetryInfo: initRetryInfo(),
- firstSuccessfulPayTimestamp: undefined,
- autoRefundDeadline: undefined,
- paymentSubmitPending: true,
- };
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.coins, Stores.purchases, Stores.proposals],
- async tx => {
- const p = await tx.get(Stores.proposals, proposal.proposalId);
- if (p) {
- p.proposalStatus = ProposalStatus.ACCEPTED;
- p.lastError = undefined;
- p.retryInfo = initRetryInfo(false);
- await tx.put(Stores.proposals, p);
- }
- await tx.put(Stores.purchases, t);
- for (let c of payCoinInfo.updatedCoins) {
- await tx.put(Stores.coins, c);
- }
- },
- );
-
- ws.notify({
- type: NotificationType.ProposalAccepted,
- proposalId: proposal.proposalId,
- });
- return t;
-}
-
-function getNextUrl(contractTerms: ContractTerms): string {
- const f = contractTerms.fulfillment_url;
- if (f.startsWith("http://") || f.startsWith("https://")) {
- const fu = new URL(contractTerms.fulfillment_url);
- fu.searchParams.set("order_id", contractTerms.order_id);
- return fu.href;
- } else {
- return f;
- }
-}
-
-export async function abortFailedPayment(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (!purchase) {
- throw Error("Purchase not found, unable to abort with refund");
- }
- if (purchase.firstSuccessfulPayTimestamp) {
- throw Error("Purchase already finished, not aborting");
- }
- if (purchase.abortDone) {
- console.warn("abort requested on already aborted purchase");
- return;
- }
-
- purchase.abortRequested = true;
-
- // From now on, we can't retry payment anymore,
- // so mark this in the DB in case the /pay abort
- // does not complete on the first try.
- await oneShotPut(ws.db, Stores.purchases, purchase);
-
- let resp;
-
- const abortReq = { ...purchase.payReq, mode: "abort-refund" };
-
- const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
-
- try {
- resp = await ws.http.postJson(payUrl, abortReq);
- } catch (e) {
- // Gives the user the option to retry / abort and refresh
- console.log("aborting payment failed", e);
- throw e;
- }
-
- if (resp.status !== 200) {
- throw Error(`unexpected status for /pay (${resp.status})`);
- }
-
- const refundResponse = MerchantRefundResponse.checked(await resp.json());
- await acceptRefundResponse(ws, purchase.proposalId, refundResponse);
-
- await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- return;
- }
- p.abortDone = true;
- await tx.put(Stores.purchases, p);
- });
-}
-
-async function incrementProposalRetry(
- ws: InternalWalletState,
- proposalId: string,
- err: OperationError | undefined,
-): Promise<void> {
- await runWithWriteTransaction(ws.db, [Stores.proposals], async tx => {
- const pr = await tx.get(Stores.proposals, proposalId);
- if (!pr) {
- return;
- }
- if (!pr.retryInfo) {
- return;
- }
- pr.retryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.retryInfo);
- pr.lastError = err;
- await tx.put(Stores.proposals, pr);
- });
- ws.notify({ type: NotificationType.ProposalOperationError });
-}
-
-async function incrementPurchasePayRetry(
- ws: InternalWalletState,
- proposalId: string,
- err: OperationError | undefined,
-): Promise<void> {
- console.log("incrementing purchase pay retry with error", err);
- await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
- const pr = await tx.get(Stores.purchases, proposalId);
- if (!pr) {
- return;
- }
- if (!pr.payRetryInfo) {
- return;
- }
- pr.payRetryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.payRetryInfo);
- pr.lastPayError = err;
- await tx.put(Stores.purchases, pr);
- });
- ws.notify({ type: NotificationType.PayOperationError });
-}
-
-async function incrementPurchaseQueryRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
- err: OperationError | undefined,
-): Promise<void> {
- console.log("incrementing purchase refund query retry with error", err);
- await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
- const pr = await tx.get(Stores.purchases, proposalId);
- if (!pr) {
- return;
- }
- if (!pr.refundStatusRetryInfo) {
- return;
- }
- pr.refundStatusRetryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.refundStatusRetryInfo);
- pr.lastRefundStatusError = err;
- await tx.put(Stores.purchases, pr);
- });
- ws.notify({ type: NotificationType.RefundStatusOperationError });
-}
-
-async function incrementPurchaseApplyRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
- err: OperationError | undefined,
-): Promise<void> {
- console.log("incrementing purchase refund apply retry with error", err);
- await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
- const pr = await tx.get(Stores.purchases, proposalId);
- if (!pr) {
- return;
- }
- if (!pr.refundApplyRetryInfo) {
- return;
- }
- pr.refundApplyRetryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.refundStatusRetryInfo);
- pr.lastRefundApplyError = err;
- await tx.put(Stores.purchases, pr);
- });
- ws.notify({ type: NotificationType.RefundApplyOperationError });
-}
-
-export async function processDownloadProposal(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean = false,
-): Promise<void> {
- const onOpErr = (err: OperationError) =>
- incrementProposalRetry(ws, proposalId, err);
- await guardOperationException(
- () => processDownloadProposalImpl(ws, proposalId, forceNow),
- onOpErr,
- );
-}
-
-async function resetDownloadProposalRetry(
- ws: InternalWalletState,
- proposalId: string,
-) {
- await oneShotMutate(ws.db, Stores.proposals, proposalId, x => {
- if (x.retryInfo.active) {
- x.retryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processDownloadProposalImpl(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean,
-): Promise<void> {
- if (forceNow) {
- await resetDownloadProposalRetry(ws, proposalId);
- }
- const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
- if (!proposal) {
- return;
- }
- if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
- return;
- }
-
- const parsedUrl = new URL(
- getOrderDownloadUrl(proposal.merchantBaseUrl, proposal.orderId),
- );
- parsedUrl.searchParams.set("nonce", proposal.noncePub);
- const urlWithNonce = parsedUrl.href;
- console.log("downloading contract from '" + urlWithNonce + "'");
- let resp;
- try {
- resp = await ws.http.get(urlWithNonce);
- } catch (e) {
- console.log("contract download failed", e);
- throw e;
- }
-
- if (resp.status !== 200) {
- throw Error(`contract download failed with status ${resp.status}`);
- }
-
- const proposalResp = Proposal.checked(await resp.json());
-
- const contractTermsHash = await ws.cryptoApi.hashString(
- canonicalJson(proposalResp.contract_terms),
- );
-
- const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.proposals, Stores.purchases],
- async tx => {
- const p = await tx.get(Stores.proposals, proposalId);
- if (!p) {
- return;
- }
- if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
- return;
- }
- if (
- fulfillmentUrl.startsWith("http://") ||
- fulfillmentUrl.startsWith("https://")
- ) {
- const differentPurchase = await tx.getIndexed(
- Stores.purchases.fulfillmentUrlIndex,
- fulfillmentUrl,
- );
- if (differentPurchase) {
- console.log("repurchase detected");
- p.proposalStatus = ProposalStatus.REPURCHASE;
- p.repurchaseProposalId = differentPurchase.proposalId;
- await tx.put(Stores.proposals, p);
- return;
- }
- }
- p.download = {
- contractTerms: proposalResp.contract_terms,
- merchantSig: proposalResp.sig,
- contractTermsHash,
- };
- p.proposalStatus = ProposalStatus.PROPOSED;
- await tx.put(Stores.proposals, p);
- },
- );
-
- ws.notify({
- type: NotificationType.ProposalDownloaded,
- proposalId: proposal.proposalId,
- });
-}
-
-/**
- * Download a proposal and store it in the database.
- * Returns an id for it to retrieve it later.
- *
- * @param sessionId Current session ID, if the proposal is being
- * downloaded in the context of a session ID.
- */
-async function startDownloadProposal(
- ws: InternalWalletState,
- merchantBaseUrl: string,
- orderId: string,
- sessionId: string | undefined,
-): Promise<string> {
- const oldProposal = await oneShotGetIndexed(
- ws.db,
- Stores.proposals.urlAndOrderIdIndex,
- [merchantBaseUrl, orderId],
- );
- if (oldProposal) {
- await processDownloadProposal(ws, oldProposal.proposalId);
- return oldProposal.proposalId;
- }
-
- const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
- const proposalId = encodeCrock(getRandomBytes(32));
-
- const proposalRecord: ProposalRecord = {
- download: undefined,
- noncePriv: priv,
- noncePub: pub,
- timestamp: getTimestampNow(),
- merchantBaseUrl,
- orderId,
- proposalId: proposalId,
- proposalStatus: ProposalStatus.DOWNLOADING,
- repurchaseProposalId: undefined,
- retryInfo: initRetryInfo(),
- lastError: undefined,
- downloadSessionId: sessionId,
- };
-
- await runWithWriteTransaction(ws.db, [Stores.proposals], async (tx) => {
- const existingRecord = await tx.getIndexed(Stores.proposals.urlAndOrderIdIndex, [
- merchantBaseUrl,
- orderId,
- ]);
- if (existingRecord) {
- // Created concurrently
- return;
- }
- await tx.put(Stores.proposals, proposalRecord);
- });
-
- await processDownloadProposal(ws, proposalId);
- return proposalId;
-}
-
-export async function submitPay(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<ConfirmPayResult> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (!purchase) {
- throw Error("Purchase not found: " + proposalId);
- }
- if (purchase.abortRequested) {
- throw Error("not submitting payment for aborted purchase");
- }
- const sessionId = purchase.lastSessionId;
- let resp;
- const payReq = { ...purchase.payReq, session_id: sessionId };
-
- console.log("paying with session ID", sessionId);
-
- const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
-
- try {
- resp = await ws.http.postJson(payUrl, payReq);
- } catch (e) {
- // Gives the user the option to retry / abort and refresh
- console.log("payment failed", e);
- throw e;
- }
- if (resp.status !== 200) {
- throw Error(`unexpected status (${resp.status}) for /pay`);
- }
- const merchantResp = await resp.json();
- console.log("got success from pay URL", merchantResp);
-
- const merchantPub = purchase.contractTerms.merchant_pub;
- const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
- merchantResp.sig,
- purchase.contractTermsHash,
- merchantPub,
- );
- if (!valid) {
- console.error("merchant payment signature invalid");
- // FIXME: properly display error
- throw Error("merchant payment signature invalid");
- }
- const isFirst = purchase.firstSuccessfulPayTimestamp === undefined;
- purchase.firstSuccessfulPayTimestamp = getTimestampNow();
- purchase.paymentSubmitPending = false;
- purchase.lastPayError = undefined;
- purchase.payRetryInfo = initRetryInfo(false);
- if (isFirst) {
- const ar = purchase.contractTerms.auto_refund;
- if (ar) {
- console.log("auto_refund present");
- const autoRefundDelay = extractTalerDuration(ar);
- console.log("auto_refund valid", autoRefundDelay);
- if (autoRefundDelay) {
- purchase.refundStatusRequested = true;
- purchase.refundStatusRetryInfo = initRetryInfo();
- purchase.lastRefundStatusError = undefined;
- purchase.autoRefundDeadline = {
- t_ms: getTimestampNow().t_ms + autoRefundDelay.d_ms,
- };
- }
- }
- }
-
- const modifiedCoins: CoinRecord[] = [];
- for (const pc of purchase.payReq.coins) {
- const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
- if (!c) {
- console.error("coin not found");
- throw Error("coin used in payment not found");
- }
- c.status = CoinStatus.Dirty;
- modifiedCoins.push(c);
- }
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.coins, Stores.purchases],
- async tx => {
- for (let c of modifiedCoins) {
- await tx.put(Stores.coins, c);
- }
- await tx.put(Stores.purchases, purchase);
- },
- );
-
- for (const c of purchase.payReq.coins) {
- refresh(ws, c.coin_pub).catch(e => {
- console.log("error in refreshing after payment:", e);
- });
- }
-
- const nextUrl = getNextUrl(purchase.contractTerms);
- ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = {
- nextUrl,
- lastSessionId: sessionId,
- };
-
- return { nextUrl };
-}
-
-/**
- * Check if a payment for the given taler://pay/ URI is possible.
- *
- * If the payment is possible, the signature are already generated but not
- * yet send to the merchant.
- */
-export async function preparePay(
- ws: InternalWalletState,
- talerPayUri: string,
-): Promise<PreparePayResult> {
- const uriResult = parsePayUri(talerPayUri);
-
- if (!uriResult) {
- return {
- status: "error",
- error: "URI not supported",
- };
- }
-
- let proposalId = await startDownloadProposal(
- ws,
- uriResult.merchantBaseUrl,
- uriResult.orderId,
- uriResult.sessionId,
- );
-
- let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
- if (!proposal) {
- throw Error(`could not get proposal ${proposalId}`);
- }
- if (proposal.proposalStatus === ProposalStatus.REPURCHASE) {
- const existingProposalId = proposal.repurchaseProposalId;
- if (!existingProposalId) {
- throw Error("invalid proposal state");
- }
- console.log("using existing purchase for same product");
- proposal = await oneShotGet(ws.db, Stores.proposals, existingProposalId);
- if (!proposal) {
- throw Error("existing proposal is in wrong state");
- }
- }
- const d = proposal.download;
- if (!d) {
- console.error("bad proposal", proposal);
- throw Error("proposal is in invalid state");
- }
- const contractTerms = d.contractTerms;
- const merchantSig = d.merchantSig;
- if (!contractTerms || !merchantSig) {
- throw Error("BUG: proposal is in invalid state");
- }
-
- proposalId = proposal.proposalId;
-
- // First check if we already payed for it.
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
-
- if (!purchase) {
- const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
- let wireFeeLimit;
- if (contractTerms.max_wire_fee) {
- wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee);
- } else {
- wireFeeLimit = Amounts.getZero(paymentAmount.currency);
- }
- // If not already payed, check if we could pay for it.
- const res = await getCoinsForPayment(ws, {
- allowedAuditors: contractTerms.auditors,
- allowedExchanges: contractTerms.exchanges,
- depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
- paymentAmount,
- wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
- wireFeeLimit,
- wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
- wireMethod: contractTerms.wire_method,
- });
-
- if (!res) {
- console.log("not confirming payment, insufficient coins");
- return {
- status: "insufficient-balance",
- contractTerms: contractTerms,
- proposalId: proposal.proposalId,
- };
- }
-
- // Only create speculative signature if we don't already have one for this proposal
- if (
- !ws.speculativePayData ||
- (ws.speculativePayData &&
- ws.speculativePayData.orderDownloadId !== proposalId)
- ) {
- const { exchangeUrl, cds, totalAmount } = res;
- const payCoinInfo = await ws.cryptoApi.signDeposit(
- contractTerms,
- cds,
- totalAmount,
- );
- ws.speculativePayData = {
- exchangeUrl,
- payCoinInfo,
- proposal,
- orderDownloadId: proposalId,
- };
- logger.trace("created speculative pay data for payment");
- }
-
- return {
- status: "payment-possible",
- contractTerms: contractTerms,
- proposalId: proposal.proposalId,
- totalFees: res.totalFees,
- };
- }
-
- if (uriResult.sessionId) {
- await submitPay(ws, proposalId);
- }
-
- return {
- status: "paid",
- contractTerms: purchase.contractTerms,
- nextUrl: getNextUrl(purchase.contractTerms),
- };
-}
-
-/**
- * Get the speculative pay data, but only if coins have not changed in between.
- */
-async function getSpeculativePayData(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<SpeculativePayData | undefined> {
- const sp = ws.speculativePayData;
- if (!sp) {
- return;
- }
- if (sp.orderDownloadId !== proposalId) {
- return;
- }
- const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
- const coins: CoinRecord[] = [];
- for (let coinKey of coinKeys) {
- const cc = await oneShotGet(ws.db, Stores.coins, coinKey);
- if (cc) {
- coins.push(cc);
- }
- }
- for (let i = 0; i < coins.length; i++) {
- const specCoin = sp.payCoinInfo.originalCoins[i];
- const currentCoin = coins[i];
-
- // Coin does not exist anymore!
- if (!currentCoin) {
- return;
- }
- if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0) {
- return;
- }
- }
- return sp;
-}
-
-/**
- * Add a contract to the wallet and sign coins, and send them.
- */
-export async function confirmPay(
- ws: InternalWalletState,
- proposalId: string,
- sessionIdOverride: string | undefined,
-): Promise<ConfirmPayResult> {
- logger.trace(
- `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
- );
- const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
-
- if (!proposal) {
- throw Error(`proposal with id ${proposalId} not found`);
- }
-
- const d = proposal.download;
- if (!d) {
- throw Error("proposal is in invalid state");
- }
-
- let purchase = await oneShotGet(ws.db, Stores.purchases, d.contractTermsHash);
-
- if (purchase) {
- if (
- sessionIdOverride !== undefined &&
- sessionIdOverride != purchase.lastSessionId
- ) {
- logger.trace(`changing session ID to ${sessionIdOverride}`);
- await oneShotMutate(ws.db, Stores.purchases, purchase.proposalId, x => {
- x.lastSessionId = sessionIdOverride;
- x.paymentSubmitPending = true;
- return x;
- });
- }
- logger.trace("confirmPay: submitting payment for existing purchase");
- return submitPay(ws, proposalId);
- }
-
- logger.trace("confirmPay: purchase record does not exist yet");
-
- const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
-
- let wireFeeLimit;
- if (!d.contractTerms.max_wire_fee) {
- wireFeeLimit = Amounts.getZero(contractAmount.currency);
- } else {
- wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
- }
-
- const res = await getCoinsForPayment(ws, {
- allowedAuditors: d.contractTerms.auditors,
- allowedExchanges: d.contractTerms.exchanges,
- depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee),
- paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount),
- wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1,
- wireFeeLimit,
- wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
- wireMethod: d.contractTerms.wire_method,
- });
-
- logger.trace("coin selection result", res);
-
- if (!res) {
- // Should not happen, since checkPay should be called first
- console.log("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
- }
-
- const sd = await getSpeculativePayData(ws, proposalId);
- if (!sd) {
- const { exchangeUrl, cds, totalAmount } = res;
- const payCoinInfo = await ws.cryptoApi.signDeposit(
- d.contractTerms,
- cds,
- totalAmount,
- );
- purchase = await recordConfirmPay(
- ws,
- proposal,
- payCoinInfo,
- exchangeUrl,
- sessionIdOverride,
- );
- } else {
- purchase = await recordConfirmPay(
- ws,
- sd.proposal,
- sd.payCoinInfo,
- sd.exchangeUrl,
- sessionIdOverride,
- );
- }
-
- logger.trace("confirmPay: submitting payment after creating purchase record");
- return submitPay(ws, proposalId);
-}
-
-export async function getFullRefundFees(
- ws: InternalWalletState,
- refundPermissions: MerchantRefundPermission[],
-): Promise<AmountJson> {
- if (refundPermissions.length === 0) {
- throw Error("no refunds given");
- }
- const coin0 = await oneShotGet(
- ws.db,
- Stores.coins,
- refundPermissions[0].coin_pub,
- );
- if (!coin0) {
- throw Error("coin not found");
- }
- let feeAcc = Amounts.getZero(
- Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
- );
-
- const denoms = await oneShotIterIndex(
- ws.db,
- Stores.denominations.exchangeBaseUrlIndex,
- coin0.exchangeBaseUrl,
- ).toArray();
-
- for (const rp of refundPermissions) {
- const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub);
- if (!coin) {
- throw Error("coin not found");
- }
- const denom = await oneShotGet(ws.db, Stores.denominations, [
- coin0.exchangeBaseUrl,
- coin.denomPub,
- ]);
- if (!denom) {
- throw Error(`denom not found (${coin.denomPub})`);
- }
- // FIXME: this assumes that the refund already happened.
- // When it hasn't, the refresh cost is inaccurate. To fix this,
- // we need introduce a flag to tell if a coin was refunded or
- // refreshed normally (and what about incremental refunds?)
- const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
- const refundFee = Amounts.parseOrThrow(rp.refund_fee);
- const refreshCost = getTotalRefreshCost(
- denoms,
- denom,
- Amounts.sub(refundAmount, refundFee).amount,
- );
- feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
- }
- return feeAcc;
-}
-
-async function acceptRefundResponse(
- ws: InternalWalletState,
- proposalId: string,
- refundResponse: MerchantRefundResponse,
-): Promise<void> {
- const refundPermissions = refundResponse.refund_permissions;
-
- let numNewRefunds = 0;
-
- await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- console.error("purchase not found, not adding refunds");
- return;
- }
-
- if (!p.refundStatusRequested) {
- return;
- }
-
- for (const perm of refundPermissions) {
- if (
- !p.refundsPending[perm.merchant_sig] &&
- !p.refundsDone[perm.merchant_sig]
- ) {
- p.refundsPending[perm.merchant_sig] = perm;
- numNewRefunds++;
- }
- }
-
- // Are we done with querying yet, or do we need to do another round
- // after a retry delay?
- let queryDone = true;
-
- if (numNewRefunds === 0) {
- if (
- p.autoRefundDeadline &&
- p.autoRefundDeadline.t_ms > getTimestampNow().t_ms
- ) {
- queryDone = false;
- }
- }
-
- if (queryDone) {
- p.lastRefundStatusTimestamp = getTimestampNow();
- p.lastRefundStatusError = undefined;
- p.refundStatusRetryInfo = initRetryInfo();
- p.refundStatusRequested = false;
- console.log("refund query done");
- } else {
- // No error, but we need to try again!
- p.lastRefundStatusTimestamp = getTimestampNow();
- p.refundStatusRetryInfo.retryCounter++;
- updateRetryInfoTimeout(p.refundStatusRetryInfo);
- p.lastRefundStatusError = undefined;
- console.log("refund query not done");
- }
-
- if (numNewRefunds) {
- p.lastRefundApplyError = undefined;
- p.refundApplyRetryInfo = initRetryInfo();
- }
-
- await tx.put(Stores.purchases, p);
- });
- ws.notify({
- type: NotificationType.RefundQueried,
- });
- if (numNewRefunds > 0) {
- await processPurchaseApplyRefund(ws, proposalId);
- }
-}
-
-async function startRefundQuery(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const success = await runWithWriteTransaction(
- ws.db,
- [Stores.purchases],
- async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- console.log("no purchase found for refund URL");
- return false;
- }
- p.refundStatusRequested = true;
- p.lastRefundStatusError = undefined;
- p.refundStatusRetryInfo = initRetryInfo();
- await tx.put(Stores.purchases, p);
- return true;
- },
- );
-
- if (!success) {
- return;
- }
-
- ws.notify({
- type: NotificationType.RefundStarted,
- });
-
- await processPurchaseQueryRefund(ws, proposalId);
-}
-
-/**
- * Accept a refund, return the contract hash for the contract
- * that was involved in the refund.
- */
-export async function applyRefund(
- ws: InternalWalletState,
- talerRefundUri: string,
-): Promise<string> {
- const parseResult = parseRefundUri(talerRefundUri);
-
- console.log("applying refund");
-
- if (!parseResult) {
- throw Error("invalid refund URI");
- }
-
- const purchase = await oneShotGetIndexed(
- ws.db,
- Stores.purchases.orderIdIndex,
- [parseResult.merchantBaseUrl, parseResult.orderId],
- );
-
- if (!purchase) {
- throw Error("no purchase for the taler://refund/ URI was found");
- }
-
- console.log("processing purchase for refund");
- await startRefundQuery(ws, purchase.proposalId);
-
- return purchase.contractTermsHash;
-}
-
-export async function processPurchasePay(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean = false,
-): Promise<void> {
- const onOpErr = (e: OperationError) =>
- incrementPurchasePayRetry(ws, proposalId, e);
- await guardOperationException(
- () => processPurchasePayImpl(ws, proposalId, forceNow),
- onOpErr,
- );
-}
-
-async function resetPurchasePayRetry(
- ws: InternalWalletState,
- proposalId: string,
-) {
- await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
- if (x.payRetryInfo.active) {
- x.payRetryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processPurchasePayImpl(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean,
-): Promise<void> {
- if (forceNow) {
- await resetPurchasePayRetry(ws, proposalId);
- }
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (!purchase) {
- return;
- }
- if (!purchase.paymentSubmitPending) {
- return;
- }
- logger.trace(`processing purchase pay ${proposalId}`);
- await submitPay(ws, proposalId);
-}
-
-export async function processPurchaseQueryRefund(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean = false,
-): Promise<void> {
- const onOpErr = (e: OperationError) =>
- incrementPurchaseQueryRefundRetry(ws, proposalId, e);
- await guardOperationException(
- () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
- onOpErr,
- );
-}
-
-async function resetPurchaseQueryRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
-) {
- await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
- if (x.refundStatusRetryInfo.active) {
- x.refundStatusRetryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processPurchaseQueryRefundImpl(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean,
-): Promise<void> {
- if (forceNow) {
- await resetPurchaseQueryRefundRetry(ws, proposalId);
- }
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (!purchase) {
- return;
- }
- if (!purchase.refundStatusRequested) {
- return;
- }
-
- const refundUrlObj = new URL(
- "refund",
- purchase.contractTerms.merchant_base_url,
- );
- refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
- const refundUrl = refundUrlObj.href;
- let resp;
- try {
- resp = await ws.http.get(refundUrl);
- } catch (e) {
- console.error("error downloading refund permission", e);
- throw e;
- }
- if (resp.status !== 200) {
- throw Error(`unexpected status code (${resp.status}) for /refund`);
- }
-
- const refundResponse = MerchantRefundResponse.checked(await resp.json());
- await acceptRefundResponse(ws, proposalId, refundResponse);
-}
-
-export async function processPurchaseApplyRefund(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean = false,
-): Promise<void> {
- const onOpErr = (e: OperationError) =>
- incrementPurchaseApplyRefundRetry(ws, proposalId, e);
- await guardOperationException(
- () => processPurchaseApplyRefundImpl(ws, proposalId, forceNow),
- onOpErr,
- );
-}
-
-async function resetPurchaseApplyRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
-) {
- await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
- if (x.refundApplyRetryInfo.active) {
- x.refundApplyRetryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processPurchaseApplyRefundImpl(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean,
-): Promise<void> {
- if (forceNow) {
- await resetPurchaseApplyRefundRetry(ws, proposalId);
- }
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (!purchase) {
- console.error("not submitting refunds, payment not found:");
- return;
- }
- const pendingKeys = Object.keys(purchase.refundsPending);
- if (pendingKeys.length === 0) {
- console.log("no pending refunds");
- return;
- }
- for (const pk of pendingKeys) {
- const perm = purchase.refundsPending[pk];
- const req: RefundRequest = {
- coin_pub: perm.coin_pub,
- h_contract_terms: purchase.contractTermsHash,
- merchant_pub: purchase.contractTerms.merchant_pub,
- merchant_sig: perm.merchant_sig,
- refund_amount: perm.refund_amount,
- refund_fee: perm.refund_fee,
- rtransaction_id: perm.rtransaction_id,
- };
- console.log("sending refund permission", perm);
- // FIXME: not correct once we support multiple exchanges per payment
- const exchangeUrl = purchase.payReq.coins[0].exchange_url;
- const reqUrl = new URL("refund", exchangeUrl);
- const resp = await ws.http.postJson(reqUrl.href, req);
- console.log("sent refund permission");
- if (resp.status !== 200) {
- console.error("refund failed", resp);
- continue;
- }
-
- let allRefundsProcessed = false;
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.purchases, Stores.coins],
- async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- return;
- }
- if (p.refundsPending[pk]) {
- p.refundsDone[pk] = p.refundsPending[pk];
- delete p.refundsPending[pk];
- }
- if (Object.keys(p.refundsPending).length === 0) {
- p.refundStatusRetryInfo = initRetryInfo();
- p.lastRefundStatusError = undefined;
- allRefundsProcessed = true;
- }
- await tx.put(Stores.purchases, p);
- const c = await tx.get(Stores.coins, perm.coin_pub);
- if (!c) {
- console.warn("coin not found, can't apply refund");
- return;
- }
- const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
- const refundFee = Amounts.parseOrThrow(perm.refund_fee);
- c.status = CoinStatus.Dirty;
- c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
- c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
- await tx.put(Stores.coins, c);
- },
- );
- if (allRefundsProcessed) {
- ws.notify({
- type: NotificationType.RefundFinished,
- });
- }
- await refresh(ws, perm.coin_pub);
- }
-
- ws.notify({
- type: NotificationType.RefundsSubmitted,
- proposalId,
- });
-}
diff --git a/src/wallet-impl/payback.ts b/src/wallet-impl/payback.ts
deleted file mode 100644
index 8cdfbf7ed..000000000
--- a/src/wallet-impl/payback.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- oneShotIter,
- runWithWriteTransaction,
- oneShotGet,
- oneShotPut,
-} from "../util/query";
-import { InternalWalletState } from "./state";
-import { Stores, TipRecord, CoinStatus } from "../dbTypes";
-
-import { Logger } from "../util/logging";
-import { PaybackConfirmation } from "../talerTypes";
-import { updateExchangeFromUrl } from "./exchanges";
-import { NotificationType } from "../walletTypes";
-
-const logger = new Logger("payback.ts");
-
-export async function payback(
- ws: InternalWalletState,
- coinPub: string,
-): Promise<void> {
- let coin = await oneShotGet(ws.db, Stores.coins, coinPub);
- if (!coin) {
- throw Error(`Coin ${coinPub} not found, can't request payback`);
- }
- const reservePub = coin.reservePub;
- if (!reservePub) {
- throw Error(`Can't request payback for a refreshed coin`);
- }
- const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
- if (!reserve) {
- throw Error(`Reserve of coin ${coinPub} not found`);
- }
- switch (coin.status) {
- case CoinStatus.Dormant:
- throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
- }
- coin.status = CoinStatus.Dormant;
- // Even if we didn't get the payback yet, we suspend withdrawal, since
- // technically we might update reserve status before we get the response
- // from the reserve for the payback request.
- reserve.hasPayback = true;
- await runWithWriteTransaction(
- ws.db,
- [Stores.coins, Stores.reserves],
- async tx => {
- await tx.put(Stores.coins, coin!!);
- await tx.put(Stores.reserves, reserve);
- },
- );
- ws.notify({
- type: NotificationType.PaybackStarted,
- });
-
- const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
- const reqUrl = new URL("payback", coin.exchangeBaseUrl);
- const resp = await ws.http.postJson(reqUrl.href, paybackRequest);
- if (resp.status !== 200) {
- throw Error();
- }
- const paybackConfirmation = PaybackConfirmation.checked(await resp.json());
- if (paybackConfirmation.reserve_pub !== coin.reservePub) {
- throw Error(`Coin's reserve doesn't match reserve on payback`);
- }
- coin = await oneShotGet(ws.db, Stores.coins, coinPub);
- if (!coin) {
- throw Error(`Coin ${coinPub} not found, can't confirm payback`);
- }
- coin.status = CoinStatus.Dormant;
- await oneShotPut(ws.db, Stores.coins, coin);
- ws.notify({
- type: NotificationType.PaybackFinished,
- });
- await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
-}
diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts
deleted file mode 100644
index 7079fa5ff..000000000
--- a/src/wallet-impl/pending.ts
+++ /dev/null
@@ -1,452 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- PendingOperationsResponse,
- getTimestampNow,
- Timestamp,
- Duration,
-} from "../walletTypes";
-import { runWithReadTransaction, TransactionHandle } from "../util/query";
-import { InternalWalletState } from "./state";
-import {
- Stores,
- ExchangeUpdateStatus,
- ReserveRecordStatus,
- CoinStatus,
- ProposalStatus,
-} from "../dbTypes";
-
-function updateRetryDelay(
- oldDelay: Duration,
- now: Timestamp,
- retryTimestamp: Timestamp,
-): Duration {
- if (retryTimestamp.t_ms <= now.t_ms) {
- return { d_ms: 0 };
- }
- return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) };
-}
-
-async function gatherExchangePending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- if (onlyDue) {
- // FIXME: exchanges should also be updated regularly
- return;
- }
- await tx.iter(Stores.exchanges).forEach(e => {
- switch (e.updateStatus) {
- case ExchangeUpdateStatus.FINISHED:
- if (e.lastError) {
- resp.pendingOperations.push({
- type: "bug",
- givesLifeness: false,
- message:
- "Exchange record is in FINISHED state but has lastError set",
- details: {
- exchangeBaseUrl: e.baseUrl,
- },
- });
- }
- if (!e.details) {
- resp.pendingOperations.push({
- type: "bug",
- givesLifeness: false,
- message:
- "Exchange record does not have details, but no update in progress.",
- details: {
- exchangeBaseUrl: e.baseUrl,
- },
- });
- }
- if (!e.wireInfo) {
- resp.pendingOperations.push({
- type: "bug",
- givesLifeness: false,
- message:
- "Exchange record does not have wire info, but no update in progress.",
- details: {
- exchangeBaseUrl: e.baseUrl,
- },
- });
- }
- break;
- case ExchangeUpdateStatus.FETCH_KEYS:
- resp.pendingOperations.push({
- type: "exchange-update",
- givesLifeness: false,
- stage: "fetch-keys",
- exchangeBaseUrl: e.baseUrl,
- lastError: e.lastError,
- reason: e.updateReason || "unknown",
- });
- break;
- case ExchangeUpdateStatus.FETCH_WIRE:
- resp.pendingOperations.push({
- type: "exchange-update",
- givesLifeness: false,
- stage: "fetch-wire",
- exchangeBaseUrl: e.baseUrl,
- lastError: e.lastError,
- reason: e.updateReason || "unknown",
- });
- break;
- default:
- resp.pendingOperations.push({
- type: "bug",
- givesLifeness: false,
- message: "Unknown exchangeUpdateStatus",
- details: {
- exchangeBaseUrl: e.baseUrl,
- exchangeUpdateStatus: e.updateStatus,
- },
- });
- break;
- }
- });
-}
-
-async function gatherReservePending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- // FIXME: this should be optimized by using an index for "onlyDue==true".
- await tx.iter(Stores.reserves).forEach(reserve => {
- const reserveType = reserve.bankWithdrawStatusUrl ? "taler-bank" : "manual";
- if (!reserve.retryInfo.active) {
- return;
- }
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.DORMANT:
- // nothing to report as pending
- break;
- case ReserveRecordStatus.UNCONFIRMED:
- if (onlyDue) {
- break;
- }
- resp.pendingOperations.push({
- type: "reserve",
- givesLifeness: false,
- stage: reserve.reserveStatus,
- timestampCreated: reserve.created,
- reserveType,
- reservePub: reserve.reservePub,
- retryInfo: reserve.retryInfo,
- });
- break;
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- case ReserveRecordStatus.WITHDRAWING:
- case ReserveRecordStatus.QUERYING_STATUS:
- case ReserveRecordStatus.REGISTERING_BANK:
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- reserve.retryInfo.nextRetry,
- );
- if (onlyDue && reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
- return;
- }
- resp.pendingOperations.push({
- type: "reserve",
- givesLifeness: true,
- stage: reserve.reserveStatus,
- timestampCreated: reserve.created,
- reserveType,
- reservePub: reserve.reservePub,
- retryInfo: reserve.retryInfo,
- });
- break;
- default:
- resp.pendingOperations.push({
- type: "bug",
- givesLifeness: false,
- message: "Unknown reserve record status",
- details: {
- reservePub: reserve.reservePub,
- reserveStatus: reserve.reserveStatus,
- },
- });
- break;
- }
- });
-}
-
-async function gatherRefreshPending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- await tx.iter(Stores.refresh).forEach(r => {
- if (r.finishedTimestamp) {
- return;
- }
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- r.retryInfo.nextRetry,
- );
- if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
- return;
- }
- let refreshStatus: string;
- if (r.norevealIndex === undefined) {
- refreshStatus = "melt";
- } else {
- refreshStatus = "reveal";
- }
-
- resp.pendingOperations.push({
- type: "refresh",
- givesLifeness: true,
- oldCoinPub: r.meltCoinPub,
- refreshStatus,
- refreshOutputSize: r.newDenoms.length,
- refreshSessionId: r.refreshSessionId,
- });
- });
-}
-
-async function gatherCoinsPending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- // Refreshing dirty coins is always due.
- await tx.iter(Stores.coins).forEach(coin => {
- if (coin.status == CoinStatus.Dirty) {
- resp.nextRetryDelay = { d_ms: 0 };
- resp.pendingOperations.push({
- givesLifeness: true,
- type: "dirty-coin",
- coinPub: coin.coinPub,
- });
- }
- });
-}
-
-async function gatherWithdrawalPending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- await tx.iter(Stores.withdrawalSession).forEach(wsr => {
- if (wsr.finishTimestamp) {
- return;
- }
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- wsr.retryInfo.nextRetry,
- );
- if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
- return;
- }
- const numCoinsWithdrawn = wsr.withdrawn.reduce(
- (a, x) => a + (x ? 1 : 0),
- 0,
- );
- const numCoinsTotal = wsr.withdrawn.length;
- resp.pendingOperations.push({
- type: "withdraw",
- givesLifeness: true,
- numCoinsTotal,
- numCoinsWithdrawn,
- source: wsr.source,
- withdrawSessionId: wsr.withdrawSessionId,
- });
- });
-}
-
-async function gatherProposalPending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- await tx.iter(Stores.proposals).forEach(proposal => {
- if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
- if (onlyDue) {
- return;
- }
- resp.pendingOperations.push({
- type: "proposal-choice",
- givesLifeness: false,
- merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
- proposalId: proposal.proposalId,
- proposalTimestamp: proposal.timestamp,
- });
- } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- proposal.retryInfo.nextRetry,
- );
- if (onlyDue && proposal.retryInfo.nextRetry.t_ms > now.t_ms) {
- return;
- }
- resp.pendingOperations.push({
- type: "proposal-download",
- givesLifeness: true,
- merchantBaseUrl: proposal.merchantBaseUrl,
- orderId: proposal.orderId,
- proposalId: proposal.proposalId,
- proposalTimestamp: proposal.timestamp,
- lastError: proposal.lastError,
- retryInfo: proposal.retryInfo,
- });
- }
- });
-}
-
-async function gatherTipPending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- await tx.iter(Stores.tips).forEach(tip => {
- if (tip.pickedUp) {
- return;
- }
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- tip.retryInfo.nextRetry,
- );
- if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
- return;
- }
- if (tip.accepted) {
- resp.pendingOperations.push({
- type: "tip",
- givesLifeness: true,
- merchantBaseUrl: tip.merchantBaseUrl,
- tipId: tip.tipId,
- merchantTipId: tip.merchantTipId,
- });
- }
- });
-}
-
-async function gatherPurchasePending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- await tx.iter(Stores.purchases).forEach(pr => {
- if (pr.paymentSubmitPending) {
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- pr.payRetryInfo.nextRetry,
- );
- if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) {
- resp.pendingOperations.push({
- type: "pay",
- givesLifeness: true,
- isReplay: false,
- proposalId: pr.proposalId,
- retryInfo: pr.payRetryInfo,
- lastError: pr.lastPayError,
- });
- }
- }
- if (pr.refundStatusRequested) {
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- pr.refundStatusRetryInfo.nextRetry,
- );
- if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) {
- resp.pendingOperations.push({
- type: "refund-query",
- givesLifeness: true,
- proposalId: pr.proposalId,
- retryInfo: pr.refundStatusRetryInfo,
- lastError: pr.lastRefundStatusError,
- });
- }
- }
- const numRefundsPending = Object.keys(pr.refundsPending).length;
- if (numRefundsPending > 0) {
- const numRefundsDone = Object.keys(pr.refundsDone).length;
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- pr.refundApplyRetryInfo.nextRetry,
- );
- if (!onlyDue || pr.refundApplyRetryInfo.nextRetry.t_ms <= now.t_ms) {
- resp.pendingOperations.push({
- type: "refund-apply",
- numRefundsDone,
- numRefundsPending,
- givesLifeness: true,
- proposalId: pr.proposalId,
- retryInfo: pr.refundApplyRetryInfo,
- lastError: pr.lastRefundApplyError,
- });
- }
- }
- });
-}
-
-export async function getPendingOperations(
- ws: InternalWalletState,
- onlyDue: boolean = false,
-): Promise<PendingOperationsResponse> {
- const resp: PendingOperationsResponse = {
- nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER },
- pendingOperations: [],
- };
- const now = getTimestampNow();
- await runWithReadTransaction(
- ws.db,
- [
- Stores.exchanges,
- Stores.reserves,
- Stores.refresh,
- Stores.coins,
- Stores.withdrawalSession,
- Stores.proposals,
- Stores.tips,
- Stores.purchases,
- ],
- async tx => {
- await gatherExchangePending(tx, now, resp, onlyDue);
- await gatherReservePending(tx, now, resp, onlyDue);
- await gatherRefreshPending(tx, now, resp, onlyDue);
- await gatherCoinsPending(tx, now, resp, onlyDue);
- await gatherWithdrawalPending(tx, now, resp, onlyDue);
- await gatherProposalPending(tx, now, resp, onlyDue);
- await gatherTipPending(tx, now, resp, onlyDue);
- await gatherPurchasePending(tx, now, resp, onlyDue);
- },
- );
- return resp;
-}
diff --git a/src/wallet-impl/refresh.ts b/src/wallet-impl/refresh.ts
deleted file mode 100644
index a33511c34..000000000
--- a/src/wallet-impl/refresh.ts
+++ /dev/null
@@ -1,479 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { AmountJson } from "../util/amounts";
-import * as Amounts from "../util/amounts";
-import {
- DenominationRecord,
- Stores,
- CoinStatus,
- RefreshPlanchetRecord,
- CoinRecord,
- RefreshSessionRecord,
- initRetryInfo,
- updateRetryInfoTimeout,
-} from "../dbTypes";
-import { amountToPretty } from "../util/helpers";
-import {
- oneShotGet,
- oneShotMutate,
- runWithWriteTransaction,
- TransactionAbort,
- oneShotIterIndex,
-} from "../util/query";
-import { InternalWalletState } from "./state";
-import { Logger } from "../util/logging";
-import { getWithdrawDenomList } from "./withdraw";
-import { updateExchangeFromUrl } from "./exchanges";
-import {
- getTimestampNow,
- OperationError,
- NotificationType,
-} from "../walletTypes";
-import { guardOperationException } from "./errors";
-
-const logger = new Logger("refresh.ts");
-
-/**
- * Get the amount that we lose when refreshing a coin of the given denomination
- * with a certain amount left.
- *
- * If the amount left is zero, then the refresh cost
- * is also considered to be zero. If a refresh isn't possible (e.g. due to lack of
- * the right denominations), then the cost is the full amount left.
- *
- * Considers refresh fees, withdrawal fees after refresh and amounts too small
- * to refresh.
- */
-export function getTotalRefreshCost(
- denoms: DenominationRecord[],
- refreshedDenom: DenominationRecord,
- amountLeft: AmountJson,
-): AmountJson {
- const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
- .amount;
- const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
- const resultingAmount = Amounts.add(
- Amounts.getZero(withdrawAmount.currency),
- ...withdrawDenoms.map(d => d.value),
- ).amount;
- const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
- logger.trace(
- "total refresh cost for",
- amountToPretty(amountLeft),
- "is",
- amountToPretty(totalCost),
- );
- return totalCost;
-}
-
-async function refreshMelt(
- ws: InternalWalletState,
- refreshSessionId: string,
-): Promise<void> {
- const refreshSession = await oneShotGet(
- ws.db,
- Stores.refresh,
- refreshSessionId,
- );
- if (!refreshSession) {
- return;
- }
- if (refreshSession.norevealIndex !== undefined) {
- return;
- }
-
- const coin = await oneShotGet(
- ws.db,
- Stores.coins,
- refreshSession.meltCoinPub,
- );
-
- if (!coin) {
- console.error("can't melt coin, it does not exist");
- return;
- }
-
- const reqUrl = new URL("refresh/melt", refreshSession.exchangeBaseUrl);
- const meltReq = {
- coin_pub: coin.coinPub,
- confirm_sig: refreshSession.confirmSig,
- denom_pub_hash: coin.denomPubHash,
- denom_sig: coin.denomSig,
- rc: refreshSession.hash,
- value_with_fee: refreshSession.valueWithFee,
- };
- logger.trace("melt request:", meltReq);
- const resp = await ws.http.postJson(reqUrl.href, meltReq);
- if (resp.status !== 200) {
- throw Error(`unexpected status code ${resp.status} for refresh/melt`);
- }
-
- const respJson = await resp.json();
-
- logger.trace("melt response:", respJson);
-
- if (resp.status !== 200) {
- console.error(respJson);
- throw Error("refresh failed");
- }
-
- const norevealIndex = respJson.noreveal_index;
-
- if (typeof norevealIndex !== "number") {
- throw Error("invalid response");
- }
-
- refreshSession.norevealIndex = norevealIndex;
-
- await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, rs => {
- if (rs.norevealIndex !== undefined) {
- return;
- }
- if (rs.finishedTimestamp) {
- return;
- }
- rs.norevealIndex = norevealIndex;
- return rs;
- });
-
- ws.notify({
- type: NotificationType.RefreshMelted,
- });
-}
-
-async function refreshReveal(
- ws: InternalWalletState,
- refreshSessionId: string,
-): Promise<void> {
- const refreshSession = await oneShotGet(
- ws.db,
- Stores.refresh,
- refreshSessionId,
- );
- if (!refreshSession) {
- return;
- }
- const norevealIndex = refreshSession.norevealIndex;
- if (norevealIndex === undefined) {
- throw Error("can't reveal without melting first");
- }
- const privs = Array.from(refreshSession.transferPrivs);
- privs.splice(norevealIndex, 1);
-
- const planchets = refreshSession.planchetsForGammas[norevealIndex];
- if (!planchets) {
- throw Error("refresh index error");
- }
-
- const meltCoinRecord = await oneShotGet(
- ws.db,
- Stores.coins,
- refreshSession.meltCoinPub,
- );
- if (!meltCoinRecord) {
- throw Error("inconsistent database");
- }
-
- const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
-
- const linkSigs: string[] = [];
- for (let i = 0; i < refreshSession.newDenoms.length; i++) {
- const linkSig = await ws.cryptoApi.signCoinLink(
- meltCoinRecord.coinPriv,
- refreshSession.newDenomHashes[i],
- refreshSession.meltCoinPub,
- refreshSession.transferPubs[norevealIndex],
- planchets[i].coinEv,
- );
- linkSigs.push(linkSig);
- }
-
- const req = {
- coin_evs: evs,
- new_denoms_h: refreshSession.newDenomHashes,
- rc: refreshSession.hash,
- transfer_privs: privs,
- transfer_pub: refreshSession.transferPubs[norevealIndex],
- link_sigs: linkSigs,
- };
-
- const reqUrl = new URL("refresh/reveal", refreshSession.exchangeBaseUrl);
- logger.trace("reveal request:", req);
-
- let resp;
- try {
- resp = await ws.http.postJson(reqUrl.href, req);
- } catch (e) {
- console.error("got error during /refresh/reveal request");
- console.error(e);
- return;
- }
-
- logger.trace("session:", refreshSession);
- logger.trace("reveal response:", resp);
-
- if (resp.status !== 200) {
- console.error("error: /refresh/reveal returned status " + resp.status);
- return;
- }
-
- const respJson = await resp.json();
-
- if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
- console.error("/refresh/reveal did not contain ev_sigs");
- return;
- }
-
- const coins: CoinRecord[] = [];
-
- for (let i = 0; i < respJson.ev_sigs.length; i++) {
- const denom = await oneShotGet(ws.db, Stores.denominations, [
- refreshSession.exchangeBaseUrl,
- refreshSession.newDenoms[i],
- ]);
- if (!denom) {
- console.error("denom not found");
- continue;
- }
- const pc =
- refreshSession.planchetsForGammas[refreshSession.norevealIndex!][i];
- const denomSig = await ws.cryptoApi.rsaUnblind(
- respJson.ev_sigs[i].ev_sig,
- pc.blindingKey,
- denom.denomPub,
- );
- const coin: CoinRecord = {
- blindingKey: pc.blindingKey,
- coinPriv: pc.privateKey,
- coinPub: pc.publicKey,
- currentAmount: denom.value,
- denomPub: denom.denomPub,
- denomPubHash: denom.denomPubHash,
- denomSig,
- exchangeBaseUrl: refreshSession.exchangeBaseUrl,
- reservePub: undefined,
- status: CoinStatus.Fresh,
- coinIndex: -1,
- withdrawSessionId: "",
- };
-
- coins.push(coin);
- }
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.coins, Stores.refresh],
- async tx => {
- const rs = await tx.get(Stores.refresh, refreshSessionId);
- if (!rs) {
- console.log("no refresh session found");
- return;
- }
- if (rs.finishedTimestamp) {
- console.log("refresh session already finished");
- return;
- }
- rs.finishedTimestamp = getTimestampNow();
- rs.retryInfo = initRetryInfo(false);
- for (let coin of coins) {
- await tx.put(Stores.coins, coin);
- }
- await tx.put(Stores.refresh, rs);
- },
- );
- console.log("refresh finished (end of reveal)");
- ws.notify({
- type: NotificationType.RefreshRevealed,
- });
-}
-
-async function incrementRefreshRetry(
- ws: InternalWalletState,
- refreshSessionId: string,
- err: OperationError | undefined,
-): Promise<void> {
- await runWithWriteTransaction(ws.db, [Stores.refresh], async tx => {
- const r = await tx.get(Stores.refresh, refreshSessionId);
- if (!r) {
- return;
- }
- if (!r.retryInfo) {
- return;
- }
- r.retryInfo.retryCounter++;
- updateRetryInfoTimeout(r.retryInfo);
- r.lastError = err;
- await tx.put(Stores.refresh, r);
- });
- ws.notify({ type: NotificationType.RefreshOperationError });
-}
-
-export async function processRefreshSession(
- ws: InternalWalletState,
- refreshSessionId: string,
- forceNow: boolean = false,
-) {
- return ws.memoProcessRefresh.memo(refreshSessionId, async () => {
- const onOpErr = (e: OperationError) =>
- incrementRefreshRetry(ws, refreshSessionId, e);
- return guardOperationException(
- () => processRefreshSessionImpl(ws, refreshSessionId, forceNow),
- onOpErr,
- );
- });
-}
-
-async function resetRefreshSessionRetry(
- ws: InternalWalletState,
- refreshSessionId: string,
-) {
- await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, (x) => {
- if (x.retryInfo.active) {
- x.retryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processRefreshSessionImpl(
- ws: InternalWalletState,
- refreshSessionId: string,
- forceNow: boolean,
-) {
- if (forceNow) {
- await resetRefreshSessionRetry(ws, refreshSessionId);
- }
- const refreshSession = await oneShotGet(
- ws.db,
- Stores.refresh,
- refreshSessionId,
- );
- if (!refreshSession) {
- return;
- }
- if (refreshSession.finishedTimestamp) {
- return;
- }
- if (typeof refreshSession.norevealIndex !== "number") {
- await refreshMelt(ws, refreshSession.refreshSessionId);
- }
- await refreshReveal(ws, refreshSession.refreshSessionId);
- logger.trace("refresh finished");
-}
-
-export async function refresh(
- ws: InternalWalletState,
- oldCoinPub: string,
- force: boolean = false,
-): Promise<void> {
- const coin = await oneShotGet(ws.db, Stores.coins, oldCoinPub);
- if (!coin) {
- console.warn("can't refresh, coin not in database");
- return;
- }
- switch (coin.status) {
- case CoinStatus.Dirty:
- break;
- case CoinStatus.Dormant:
- return;
- case CoinStatus.Fresh:
- if (!force) {
- return;
- }
- break;
- }
-
- const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
- if (!exchange) {
- throw Error("db inconsistent: exchange of coin not found");
- }
-
- const oldDenom = await oneShotGet(ws.db, Stores.denominations, [
- exchange.baseUrl,
- coin.denomPub,
- ]);
-
- if (!oldDenom) {
- throw Error("db inconsistent: denomination for coin not found");
- }
-
- const availableDenoms: DenominationRecord[] = await oneShotIterIndex(
- ws.db,
- Stores.denominations.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
-
- const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
- .amount;
-
- const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
-
- if (newCoinDenoms.length === 0) {
- logger.trace(
- `not refreshing, available amount ${amountToPretty(
- availableAmount,
- )} too small`,
- );
- await oneShotMutate(ws.db, Stores.coins, oldCoinPub, x => {
- if (x.status != coin.status) {
- // Concurrent modification?
- return;
- }
- x.status = CoinStatus.Dormant;
- return x;
- });
- ws.notify({ type: NotificationType.RefreshRefused });
- return;
- }
-
- const refreshSession: RefreshSessionRecord = await ws.cryptoApi.createRefreshSession(
- exchange.baseUrl,
- 3,
- coin,
- newCoinDenoms,
- oldDenom.feeRefresh,
- );
-
- // Store refresh session and subtract refreshed amount from
- // coin in the same transaction.
- await runWithWriteTransaction(
- ws.db,
- [Stores.refresh, Stores.coins],
- async tx => {
- const c = await tx.get(Stores.coins, coin.coinPub);
- if (!c) {
- return;
- }
- if (c.status !== CoinStatus.Dirty) {
- return;
- }
- const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
- if (r.saturated) {
- console.log("can't refresh coin, no amount left");
- return;
- }
- c.currentAmount = r.amount;
- c.status = CoinStatus.Dormant;
- await tx.put(Stores.refresh, refreshSession);
- await tx.put(Stores.coins, c);
- },
- );
- logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
- ws.notify({ type: NotificationType.RefreshStarted });
-
- await processRefreshSession(ws, refreshSession.refreshSessionId);
-}
diff --git a/src/wallet-impl/reserves.ts b/src/wallet-impl/reserves.ts
deleted file mode 100644
index 504cf10f0..000000000
--- a/src/wallet-impl/reserves.ts
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- CreateReserveRequest,
- CreateReserveResponse,
- getTimestampNow,
- ConfirmReserveRequest,
- OperationError,
- NotificationType,
-} from "../walletTypes";
-import { canonicalizeBaseUrl } from "../util/helpers";
-import { InternalWalletState } from "./state";
-import {
- ReserveRecordStatus,
- ReserveRecord,
- CurrencyRecord,
- Stores,
- WithdrawalSessionRecord,
- initRetryInfo,
- updateRetryInfoTimeout,
-} from "../dbTypes";
-import {
- oneShotMutate,
- oneShotPut,
- oneShotGet,
- runWithWriteTransaction,
- TransactionAbort,
-} from "../util/query";
-import { Logger } from "../util/logging";
-import * as Amounts from "../util/amounts";
-import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
-import { WithdrawOperationStatusResponse, ReserveStatus } from "../talerTypes";
-import { assertUnreachable } from "../util/assertUnreachable";
-import { encodeCrock } from "../crypto/talerCrypto";
-import { randomBytes } from "../crypto/primitives/nacl-fast";
-import {
- getVerifiedWithdrawDenomList,
- processWithdrawSession,
-} from "./withdraw";
-import { guardOperationException, OperationFailedAndReportedError } from "./errors";
-
-const logger = new Logger("reserves.ts");
-
-/**
- * Create a reserve, but do not flag it as confirmed yet.
- *
- * Adds the corresponding exchange as a trusted exchange if it is neither
- * audited nor trusted already.
- */
-export async function createReserve(
- ws: InternalWalletState,
- req: CreateReserveRequest,
-): Promise<CreateReserveResponse> {
- const keypair = await ws.cryptoApi.createEddsaKeypair();
- const now = getTimestampNow();
- const canonExchange = canonicalizeBaseUrl(req.exchange);
-
- let reserveStatus;
- if (req.bankWithdrawStatusUrl) {
- reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
- } else {
- reserveStatus = ReserveRecordStatus.UNCONFIRMED;
- }
-
- const currency = req.amount.currency;
-
- const reserveRecord: ReserveRecord = {
- created: now,
- withdrawAllocatedAmount: Amounts.getZero(currency),
- withdrawCompletedAmount: Amounts.getZero(currency),
- withdrawRemainingAmount: Amounts.getZero(currency),
- exchangeBaseUrl: canonExchange,
- hasPayback: false,
- initiallyRequestedAmount: req.amount,
- reservePriv: keypair.priv,
- reservePub: keypair.pub,
- senderWire: req.senderWire,
- timestampConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
- exchangeWire: req.exchangeWire,
- reserveStatus,
- lastSuccessfulStatusQuery: undefined,
- retryInfo: initRetryInfo(),
- lastError: undefined,
- };
-
- const senderWire = req.senderWire;
- if (senderWire) {
- const rec = {
- paytoUri: senderWire,
- };
- await oneShotPut(ws.db, Stores.senderWires, rec);
- }
-
- const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
- const exchangeDetails = exchangeInfo.details;
- if (!exchangeDetails) {
- console.log(exchangeDetails);
- throw Error("exchange not updated");
- }
- const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
- let currencyRecord = await oneShotGet(
- ws.db,
- Stores.currencies,
- exchangeDetails.currency,
- );
- if (!currencyRecord) {
- currencyRecord = {
- auditors: [],
- exchanges: [],
- fractionalDigits: 2,
- name: exchangeDetails.currency,
- };
- }
-
- if (!isAudited && !isTrusted) {
- currencyRecord.exchanges.push({
- baseUrl: req.exchange,
- exchangePub: exchangeDetails.masterPublicKey,
- });
- }
-
- const cr: CurrencyRecord = currencyRecord;
-
- const resp = await runWithWriteTransaction(
- ws.db,
- [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
- async tx => {
- // Check if we have already created a reserve for that bankWithdrawStatusUrl
- if (reserveRecord.bankWithdrawStatusUrl) {
- const bwi = await tx.get(
- Stores.bankWithdrawUris,
- reserveRecord.bankWithdrawStatusUrl,
- );
- if (bwi) {
- const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
- if (otherReserve) {
- logger.trace(
- "returning existing reserve for bankWithdrawStatusUri",
- );
- return {
- exchange: otherReserve.exchangeBaseUrl,
- reservePub: otherReserve.reservePub,
- };
- }
- }
- await tx.put(Stores.bankWithdrawUris, {
- reservePub: reserveRecord.reservePub,
- talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl,
- });
- }
- await tx.put(Stores.currencies, cr);
- await tx.put(Stores.reserves, reserveRecord);
- const r: CreateReserveResponse = {
- exchange: canonExchange,
- reservePub: keypair.pub,
- };
- return r;
- },
- );
-
- ws.notify({ type: NotificationType.ReserveCreated });
-
- // Asynchronously process the reserve, but return
- // to the caller already.
- processReserve(ws, resp.reservePub, true).catch(e => {
- console.error("Processing reserve failed:", e);
- });
-
- return resp;
-}
-
-/**
- * First fetch information requred to withdraw from the reserve,
- * then deplete the reserve, withdrawing coins until it is empty.
- *
- * The returned promise resolves once the reserve is set to the
- * state DORMANT.
- */
-export async function processReserve(
- ws: InternalWalletState,
- reservePub: string,
- forceNow: boolean = false,
-): Promise<void> {
- return ws.memoProcessReserve.memo(reservePub, async () => {
- const onOpError = (err: OperationError) =>
- incrementReserveRetry(ws, reservePub, err);
- await guardOperationException(
- () => processReserveImpl(ws, reservePub, forceNow),
- onOpError,
- );
- });
-}
-
-
-async function registerReserveWithBank(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
- switch (reserve?.reserveStatus) {
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- case ReserveRecordStatus.REGISTERING_BANK:
- break;
- default:
- return;
- }
- const bankStatusUrl = reserve.bankWithdrawStatusUrl;
- if (!bankStatusUrl) {
- return;
- }
- console.log("making selection");
- if (reserve.timestampReserveInfoPosted) {
- throw Error("bank claims that reserve info selection is not done");
- }
- const bankResp = await ws.http.postJson(bankStatusUrl, {
- reserve_pub: reservePub,
- selected_exchange: reserve.exchangeWire,
- });
- console.log("got response", bankResp);
- await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
- switch (r.reserveStatus) {
- case ReserveRecordStatus.REGISTERING_BANK:
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- break;
- default:
- return;
- }
- r.timestampReserveInfoPosted = getTimestampNow();
- r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
- r.retryInfo = initRetryInfo();
- return r;
- });
- ws.notify( { type: NotificationType.Wildcard });
- return processReserveBankStatus(ws, reservePub);
-}
-
-export async function processReserveBankStatus(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const onOpError = (err: OperationError) =>
- incrementReserveRetry(ws, reservePub, err);
- await guardOperationException(
- () => processReserveBankStatusImpl(ws, reservePub),
- onOpError,
- );
-}
-
-async function processReserveBankStatusImpl(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
- switch (reserve?.reserveStatus) {
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- case ReserveRecordStatus.REGISTERING_BANK:
- break;
- default:
- return;
- }
- const bankStatusUrl = reserve.bankWithdrawStatusUrl;
- if (!bankStatusUrl) {
- return;
- }
-
- let status: WithdrawOperationStatusResponse;
- try {
- const statusResp = await ws.http.get(bankStatusUrl);
- if (statusResp.status !== 200) {
- throw Error(`unexpected status ${statusResp.status} for bank status query`);
- }
- status = WithdrawOperationStatusResponse.checked(await statusResp.json());
- } catch (e) {
- throw e;
- }
-
- ws.notify( { type: NotificationType.Wildcard });
-
- if (status.selection_done) {
- if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
- } else {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
-
- if (status.transfer_done) {
- await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
- switch (r.reserveStatus) {
- case ReserveRecordStatus.REGISTERING_BANK:
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- break;
- default:
- return;
- }
- const now = getTimestampNow();
- r.timestampConfirmed = now;
- r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
- r.retryInfo = initRetryInfo();
- return r;
- });
- await processReserveImpl(ws, reservePub, true);
- } else {
- await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
- switch (r.reserveStatus) {
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- break;
- default:
- return;
- }
- r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
- return r;
- });
- await incrementReserveRetry(ws, reservePub, undefined);
- }
- ws.notify( { type: NotificationType.Wildcard });
-}
-
-async function incrementReserveRetry(
- ws: InternalWalletState,
- reservePub: string,
- err: OperationError | undefined,
-): Promise<void> {
- await runWithWriteTransaction(ws.db, [Stores.reserves], async tx => {
- const r = await tx.get(Stores.reserves, reservePub);
- if (!r) {
- return;
- }
- if (!r.retryInfo) {
- return;
- }
- r.retryInfo.retryCounter++;
- updateRetryInfoTimeout(r.retryInfo);
- r.lastError = err;
- await tx.put(Stores.reserves, r);
- });
- ws.notify({ type: NotificationType.ReserveOperationError });
-}
-
-/**
- * Update the information about a reserve that is stored in the wallet
- * by quering the reserve's exchange.
- */
-async function updateReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
- if (!reserve) {
- throw Error("reserve not in db");
- }
-
- if (reserve.timestampConfirmed === undefined) {
- throw Error("reserve not confirmed yet");
- }
-
- if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
- return;
- }
-
- const reqUrl = new URL("reserve/status", reserve.exchangeBaseUrl);
- reqUrl.searchParams.set("reserve_pub", reservePub);
- let resp;
- try {
- resp = await ws.http.get(reqUrl.href);
- if (resp.status === 404) {
- const m = "The exchange does not know about this reserve (yet).";
- await incrementReserveRetry(ws, reservePub, undefined);
- return;
- }
- if (resp.status !== 200) {
- throw Error(`unexpected status code ${resp.status} for reserve/status`)
- }
- } catch (e) {
- const m = e.message;
- await incrementReserveRetry(ws, reservePub, {
- type: "network",
- details: {},
- message: m,
- });
- throw new OperationFailedAndReportedError(m);
- }
- const reserveInfo = ReserveStatus.checked(await resp.json());
- const balance = Amounts.parseOrThrow(reserveInfo.balance);
- await oneShotMutate(ws.db, Stores.reserves, reserve.reservePub, r => {
- if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
- return;
- }
-
- // FIXME: check / compare history!
- if (!r.lastSuccessfulStatusQuery) {
- // FIXME: check if this matches initial expectations
- r.withdrawRemainingAmount = balance;
- } else {
- const expectedBalance = Amounts.sub(
- r.withdrawAllocatedAmount,
- r.withdrawCompletedAmount,
- );
- const cmp = Amounts.cmp(balance, expectedBalance.amount);
- if (cmp == 0) {
- // Nothing changed.
- return;
- }
- if (cmp > 0) {
- const extra = Amounts.sub(balance, expectedBalance.amount).amount;
- r.withdrawRemainingAmount = Amounts.add(
- r.withdrawRemainingAmount,
- extra,
- ).amount;
- } else {
- // We're missing some money.
- }
- }
- r.lastSuccessfulStatusQuery = getTimestampNow();
- r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
- r.retryInfo = initRetryInfo();
- return r;
- });
- ws.notify( { type: NotificationType.ReserveUpdated });
-}
-
-async function processReserveImpl(
- ws: InternalWalletState,
- reservePub: string,
- forceNow: boolean = false,
-): Promise<void> {
- const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
- if (!reserve) {
- console.log("not processing reserve: reserve does not exist");
- return;
- }
- if (!forceNow) {
- const now = getTimestampNow();
- if (reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
- logger.trace("processReserve retry not due yet");
- return;
- }
- }
- logger.trace(
- `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
- );
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.UNCONFIRMED:
- // nothing to do
- break;
- case ReserveRecordStatus.REGISTERING_BANK:
- await processReserveBankStatus(ws, reservePub);
- return processReserveImpl(ws, reservePub, true);
- case ReserveRecordStatus.QUERYING_STATUS:
- await updateReserve(ws, reservePub);
- return processReserveImpl(ws, reservePub, true);
- case ReserveRecordStatus.WITHDRAWING:
- await depleteReserve(ws, reservePub);
- break;
- case ReserveRecordStatus.DORMANT:
- // nothing to do
- break;
- case ReserveRecordStatus.WAIT_CONFIRM_BANK:
- await processReserveBankStatus(ws, reservePub);
- break;
- default:
- console.warn("unknown reserve record status:", reserve.reserveStatus);
- assertUnreachable(reserve.reserveStatus);
- break;
- }
-}
-
-export async function confirmReserve(
- ws: InternalWalletState,
- req: ConfirmReserveRequest,
-): Promise<void> {
- const now = getTimestampNow();
- await oneShotMutate(ws.db, Stores.reserves, req.reservePub, reserve => {
- if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
- return;
- }
- reserve.timestampConfirmed = now;
- reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
- reserve.retryInfo = initRetryInfo();
- return reserve;
- });
-
- ws.notify({ type: NotificationType.ReserveUpdated });
-
- processReserve(ws, req.reservePub, true).catch(e => {
- console.log("processing reserve failed:", e);
- });
-}
-
-/**
- * Withdraw coins from a reserve until it is empty.
- *
- * When finished, marks the reserve as depleted by setting
- * the depleted timestamp.
- */
-async function depleteReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
- if (!reserve) {
- return;
- }
- if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
- return;
- }
- logger.trace(`depleting reserve ${reservePub}`);
-
- const withdrawAmount = reserve.withdrawRemainingAmount;
-
- logger.trace(`getting denom list`);
-
- const denomsForWithdraw = await getVerifiedWithdrawDenomList(
- ws,
- reserve.exchangeBaseUrl,
- withdrawAmount,
- );
- logger.trace(`got denom list`);
- if (denomsForWithdraw.length === 0) {
- const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
- await incrementReserveRetry(ws, reserve.reservePub, {
- type: "internal",
- message: m,
- details: {},
- });
- console.log(m);
- throw new OperationFailedAndReportedError(m);
- }
-
- logger.trace("selected denominations");
-
- const withdrawalSessionId = encodeCrock(randomBytes(32));
-
- const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
- .amount;
-
- const withdrawalRecord: WithdrawalSessionRecord = {
- withdrawSessionId: withdrawalSessionId,
- exchangeBaseUrl: reserve.exchangeBaseUrl,
- source: {
- type: "reserve",
- reservePub: reserve.reservePub,
- },
- rawWithdrawalAmount: withdrawAmount,
- startTimestamp: getTimestampNow(),
- denoms: denomsForWithdraw.map(x => x.denomPub),
- withdrawn: denomsForWithdraw.map(x => false),
- planchets: denomsForWithdraw.map(x => undefined),
- totalCoinValue,
- retryInfo: initRetryInfo(),
- lastCoinErrors: denomsForWithdraw.map(x => undefined),
- lastError: undefined,
- };
-
- const totalCoinWithdrawFee = Amounts.sum(
- denomsForWithdraw.map(x => x.feeWithdraw),
- ).amount;
- const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
- .amount;
-
- function mutateReserve(r: ReserveRecord): ReserveRecord {
- const remaining = Amounts.sub(
- r.withdrawRemainingAmount,
- totalWithdrawAmount,
- );
- if (remaining.saturated) {
- console.error("can't create planchets, saturated");
- throw TransactionAbort;
- }
- const allocated = Amounts.add(
- r.withdrawAllocatedAmount,
- totalWithdrawAmount,
- );
- if (allocated.saturated) {
- console.error("can't create planchets, saturated");
- throw TransactionAbort;
- }
- r.withdrawRemainingAmount = remaining.amount;
- r.withdrawAllocatedAmount = allocated.amount;
- r.reserveStatus = ReserveRecordStatus.DORMANT;
- r.retryInfo = initRetryInfo(false);
- return r;
- }
-
- const success = await runWithWriteTransaction(
- ws.db,
- [Stores.withdrawalSession, Stores.reserves],
- async tx => {
- const myReserve = await tx.get(Stores.reserves, reservePub);
- if (!myReserve) {
- return false;
- }
- if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
- return false;
- }
- await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
- await tx.put(Stores.withdrawalSession, withdrawalRecord);
- return true;
- },
- );
-
- if (success) {
- console.log("processing new withdraw session");
- ws.notify({
- type: NotificationType.WithdrawSessionCreated,
- withdrawSessionId: withdrawalSessionId,
- });
- await processWithdrawSession(ws, withdrawalSessionId);
- } else {
- console.trace("withdraw session already existed");
- }
-}
diff --git a/src/wallet-impl/return.ts b/src/wallet-impl/return.ts
deleted file mode 100644
index 0c142f9a6..000000000
--- a/src/wallet-impl/return.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- HistoryQuery,
- HistoryEvent,
- WalletBalance,
- WalletBalanceEntry,
- ReturnCoinsRequest,
- CoinWithDenom,
-} from "../walletTypes";
-import { oneShotIter, runWithWriteTransaction, oneShotGet, oneShotIterIndex, oneShotPut } from "../util/query";
-import { InternalWalletState } from "./state";
-import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from "../dbTypes";
-import * as Amounts from "../util/amounts";
-import { AmountJson } from "../util/amounts";
-import { Logger } from "../util/logging";
-import { canonicalJson } from "../util/helpers";
-import { ContractTerms } from "../talerTypes";
-import { selectPayCoins } from "./pay";
-
-const logger = new Logger("return.ts");
-
-async function getCoinsForReturn(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- amount: AmountJson,
-): Promise<CoinWithDenom[] | undefined> {
- const exchange = await oneShotGet(
- ws.db,
- Stores.exchanges,
- exchangeBaseUrl,
- );
- if (!exchange) {
- throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
- }
-
- const coins: CoinRecord[] = await oneShotIterIndex(
- ws.db,
- Stores.coins.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
-
- if (!coins || !coins.length) {
- return [];
- }
-
- const denoms = await oneShotIterIndex(
- ws.db,
- Stores.denominations.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
-
- // Denomination of the first coin, we assume that all other
- // coins have the same currency
- const firstDenom = await oneShotGet(ws.db, Stores.denominations, [
- exchange.baseUrl,
- coins[0].denomPub,
- ]);
- if (!firstDenom) {
- throw Error("db inconsistent");
- }
- const currency = firstDenom.value.currency;
-
- const cds: CoinWithDenom[] = [];
- for (const coin of coins) {
- const denom = await oneShotGet(ws.db, Stores.denominations, [
- exchange.baseUrl,
- coin.denomPub,
- ]);
- if (!denom) {
- throw Error("db inconsistent");
- }
- if (denom.value.currency !== currency) {
- console.warn(
- `same pubkey for different currencies at exchange ${exchange.baseUrl}`,
- );
- continue;
- }
- if (coin.suspended) {
- continue;
- }
- if (coin.status !== CoinStatus.Fresh) {
- continue;
- }
- cds.push({ coin, denom });
- }
-
- const res = selectPayCoins(denoms, cds, amount, amount);
- if (res) {
- return res.cds;
- }
- return undefined;
-}
-
-
-/**
- * Trigger paying coins back into the user's account.
- */
-export async function returnCoins(
- ws: InternalWalletState,
- req: ReturnCoinsRequest,
-): Promise<void> {
- logger.trace("got returnCoins request", req);
- const wireType = (req.senderWire as any).type;
- logger.trace("wireType", wireType);
- if (!wireType || typeof wireType !== "string") {
- console.error(`wire type must be a non-empty string, not ${wireType}`);
- return;
- }
- const stampSecNow = Math.floor(new Date().getTime() / 1000);
- const exchange = await oneShotGet(ws.db, Stores.exchanges, req.exchange);
- if (!exchange) {
- console.error(`Exchange ${req.exchange} not known to the wallet`);
- return;
- }
- const exchangeDetails = exchange.details;
- if (!exchangeDetails) {
- throw Error("exchange information needs to be updated first.");
- }
- logger.trace("selecting coins for return:", req);
- const cds = await getCoinsForReturn(ws, req.exchange, req.amount);
- logger.trace(cds);
-
- if (!cds) {
- throw Error("coin return impossible, can't select coins");
- }
-
- const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
-
- const wireHash = await ws.cryptoApi.hashString(
- canonicalJson(req.senderWire),
- );
-
- const contractTerms: ContractTerms = {
- H_wire: wireHash,
- amount: Amounts.toString(req.amount),
- auditors: [],
- exchanges: [
- { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
- ],
- extra: {},
- fulfillment_url: "",
- locations: [],
- max_fee: Amounts.toString(req.amount),
- merchant: {},
- merchant_pub: pub,
- order_id: "none",
- pay_deadline: `/Date(${stampSecNow + 30 * 5})/`,
- wire_transfer_deadline: `/Date(${stampSecNow + 60 * 5})/`,
- merchant_base_url: "taler://return-to-account",
- products: [],
- refund_deadline: `/Date(${stampSecNow + 60 * 5})/`,
- timestamp: `/Date(${stampSecNow})/`,
- wire_method: wireType,
- };
-
- const contractTermsHash = await ws.cryptoApi.hashString(
- canonicalJson(contractTerms),
- );
-
- const payCoinInfo = await ws.cryptoApi.signDeposit(
- contractTerms,
- cds,
- Amounts.parseOrThrow(contractTerms.amount),
- );
-
- logger.trace("pci", payCoinInfo);
-
- const coins = payCoinInfo.sigs.map(s => ({ coinPaySig: s }));
-
- const coinsReturnRecord: CoinsReturnRecord = {
- coins,
- contractTerms,
- contractTermsHash,
- exchange: exchange.baseUrl,
- merchantPriv: priv,
- wire: req.senderWire,
- };
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.coinsReturns, Stores.coins],
- async tx => {
- await tx.put(Stores.coinsReturns, coinsReturnRecord);
- for (let c of payCoinInfo.updatedCoins) {
- await tx.put(Stores.coins, c);
- }
- },
- );
-
- depositReturnedCoins(ws, coinsReturnRecord);
-}
-
-async function depositReturnedCoins(
- ws: InternalWalletState,
- coinsReturnRecord: CoinsReturnRecord,
-): Promise<void> {
- for (const c of coinsReturnRecord.coins) {
- if (c.depositedSig) {
- continue;
- }
- const req = {
- H_wire: coinsReturnRecord.contractTerms.H_wire,
- coin_pub: c.coinPaySig.coin_pub,
- coin_sig: c.coinPaySig.coin_sig,
- contribution: c.coinPaySig.contribution,
- denom_pub: c.coinPaySig.denom_pub,
- h_contract_terms: coinsReturnRecord.contractTermsHash,
- merchant_pub: coinsReturnRecord.contractTerms.merchant_pub,
- pay_deadline: coinsReturnRecord.contractTerms.pay_deadline,
- refund_deadline: coinsReturnRecord.contractTerms.refund_deadline,
- timestamp: coinsReturnRecord.contractTerms.timestamp,
- ub_sig: c.coinPaySig.ub_sig,
- wire: coinsReturnRecord.wire,
- wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline,
- };
- logger.trace("req", req);
- const reqUrl = new URL("deposit", coinsReturnRecord.exchange);
- const resp = await ws.http.postJson(reqUrl.href, req);
- if (resp.status !== 200) {
- console.error("deposit failed due to status code", resp);
- continue;
- }
- const respJson = await resp.json();
- if (respJson.status !== "DEPOSIT_OK") {
- console.error("deposit failed", resp);
- continue;
- }
-
- if (!respJson.sig) {
- console.error("invalid 'sig' field", resp);
- continue;
- }
-
- // FIXME: verify signature
-
- // For every successful deposit, we replace the old record with an updated one
- const currentCrr = await oneShotGet(
- ws.db,
- Stores.coinsReturns,
- coinsReturnRecord.contractTermsHash,
- );
- if (!currentCrr) {
- console.error("database inconsistent");
- continue;
- }
- for (const nc of currentCrr.coins) {
- if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) {
- nc.depositedSig = respJson.sig;
- }
- }
- await oneShotPut(ws.db, Stores.coinsReturns, currentCrr);
- }
-}
diff --git a/src/wallet-impl/state.ts b/src/wallet-impl/state.ts
deleted file mode 100644
index 18df861f1..000000000
--- a/src/wallet-impl/state.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { HttpRequestLibrary } from "../util/http";
-import {
- NextUrlResult,
- WalletBalance,
- PendingOperationsResponse,
- WalletNotification,
-} from "../walletTypes";
-import { SpeculativePayData } from "./pay";
-import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
-import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
-import { Logger } from "../util/logging";
-
-type NotificationListener = (n: WalletNotification) => void;
-
-const logger = new Logger("state.ts");
-
-export class InternalWalletState {
- speculativePayData: SpeculativePayData | undefined = undefined;
- cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
- memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
- memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
- memoGetPending: AsyncOpMemoSingle<
- PendingOperationsResponse
- > = new AsyncOpMemoSingle();
- memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle();
- memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
- cryptoApi: CryptoApi;
-
- listeners: NotificationListener[] = [];
-
- constructor(
- public db: IDBDatabase,
- public http: HttpRequestLibrary,
- cryptoWorkerFactory: CryptoWorkerFactory,
- ) {
- this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
- }
-
- public notify(n: WalletNotification) {
- logger.trace("Notification", n);
- for (const l of this.listeners) {
- const nc = JSON.parse(JSON.stringify(n));
- setImmediate(() => {
- l(nc);
- });
- }
- }
-
- addNotificationListener(f: (n: WalletNotification) => void): void {
- this.listeners.push(f);
- }
-}
diff --git a/src/wallet-impl/tip.ts b/src/wallet-impl/tip.ts
deleted file mode 100644
index 22ec37793..000000000
--- a/src/wallet-impl/tip.ts
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-
-import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from "../util/query";
-import { InternalWalletState } from "./state";
-import { parseTipUri } from "../util/taleruri";
-import { TipStatus, getTimestampNow, OperationError, NotificationType } from "../walletTypes";
-import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTypes";
-import * as Amounts from "../util/amounts";
-import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes";
-import { getExchangeWithdrawalInfo, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw";
-import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers";
-import { updateExchangeFromUrl } from "./exchanges";
-import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
-import { guardOperationException } from "./errors";
-
-
-export async function getTipStatus(
- ws: InternalWalletState,
- talerTipUri: string): Promise<TipStatus> {
- const res = parseTipUri(talerTipUri);
- if (!res) {
- throw Error("invalid taler://tip URI");
- }
-
- const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl);
- tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
- console.log("checking tip status from", tipStatusUrl.href);
- const merchantResp = await ws.http.get(tipStatusUrl.href);
- if (merchantResp.status !== 200) {
- throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
- }
- const respJson = await merchantResp.json();
- console.log("resp:", respJson);
- const tipPickupStatus = TipPickupGetResponse.checked(respJson);
-
- console.log("status", tipPickupStatus);
-
- let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
-
- let tipRecord = await oneShotGet(ws.db, Stores.tips, [
- res.merchantTipId,
- res.merchantOrigin,
- ]);
-
- if (!tipRecord) {
- const withdrawDetails = await getExchangeWithdrawalInfo(
- ws,
- tipPickupStatus.exchange_url,
- amount,
- );
-
- const tipId = encodeCrock(getRandomBytes(32));
-
- tipRecord = {
- tipId,
- accepted: false,
- amount,
- deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
- exchangeUrl: tipPickupStatus.exchange_url,
- merchantBaseUrl: res.merchantBaseUrl,
- nextUrl: undefined,
- pickedUp: false,
- planchets: undefined,
- response: undefined,
- createdTimestamp: getTimestampNow(),
- merchantTipId: res.merchantTipId,
- totalFees: Amounts.add(
- withdrawDetails.overhead,
- withdrawDetails.withdrawFee,
- ).amount,
- retryInfo: initRetryInfo(),
- lastError: undefined,
- };
- await oneShotPut(ws.db, Stores.tips, tipRecord);
- }
-
- const tipStatus: TipStatus = {
- accepted: !!tipRecord && tipRecord.accepted,
- amount: Amounts.parseOrThrow(tipPickupStatus.amount),
- amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
- exchangeUrl: tipPickupStatus.exchange_url,
- nextUrl: tipPickupStatus.extra.next_url,
- merchantOrigin: res.merchantOrigin,
- merchantTipId: res.merchantTipId,
- expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
- timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
- totalFees: tipRecord.totalFees,
- tipId: tipRecord.tipId,
- };
-
- return tipStatus;
-}
-
-async function incrementTipRetry(
- ws: InternalWalletState,
- refreshSessionId: string,
- err: OperationError | undefined,
-): Promise<void> {
- await runWithWriteTransaction(ws.db, [Stores.tips], async tx => {
- const t = await tx.get(Stores.tips, refreshSessionId);
- if (!t) {
- return;
- }
- if (!t.retryInfo) {
- return;
- }
- t.retryInfo.retryCounter++;
- updateRetryInfoTimeout(t.retryInfo);
- t.lastError = err;
- await tx.put(Stores.tips, t);
- });
- ws.notify({ type: NotificationType.TipOperationError });
-}
-
-export async function processTip(
- ws: InternalWalletState,
- tipId: string,
- forceNow: boolean = false,
-): Promise<void> {
- const onOpErr = (e: OperationError) => incrementTipRetry(ws, tipId, e);
- await guardOperationException(() => processTipImpl(ws, tipId, forceNow), onOpErr);
-}
-
-async function resetTipRetry(
- ws: InternalWalletState,
- tipId: string,
-): Promise<void> {
- await oneShotMutate(ws.db, Stores.tips, tipId, (x) => {
- if (x.retryInfo.active) {
- x.retryInfo = initRetryInfo();
- }
- return x;
- })
-}
-
-async function processTipImpl(
- ws: InternalWalletState,
- tipId: string,
- forceNow: boolean,
-) {
- if (forceNow) {
- await resetTipRetry(ws, tipId);
- }
- let tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
- if (!tipRecord) {
- return;
- }
-
- if (tipRecord.pickedUp) {
- console.log("tip already picked up");
- return;
- }
-
- if (!tipRecord.planchets) {
- await updateExchangeFromUrl(ws, tipRecord.exchangeUrl);
- const denomsForWithdraw = await getVerifiedWithdrawDenomList(
- ws,
- tipRecord.exchangeUrl,
- tipRecord.amount,
- );
-
- const planchets = await Promise.all(
- denomsForWithdraw.map(d => ws.cryptoApi.createTipPlanchet(d)),
- );
-
- await oneShotMutate(ws.db, Stores.tips, tipId, r => {
- if (!r.planchets) {
- r.planchets = planchets;
- }
- return r;
- });
- }
-
- tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
- if (!tipRecord) {
- throw Error("tip not in database");
- }
-
- if (!tipRecord.planchets) {
- throw Error("invariant violated");
- }
-
- console.log("got planchets for tip!");
-
- // Planchets in the form that the merchant expects
- const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map(p => ({
- coin_ev: p.coinEv,
- denom_pub_hash: p.denomPubHash,
- }));
-
- let merchantResp;
-
- const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl);
-
- try {
- const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId };
- merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
- if (merchantResp.status !== 200) {
- throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
- }
- console.log("got merchant resp:", merchantResp);
- } catch (e) {
- console.log("tipping failed", e);
- throw e;
- }
-
- const response = TipResponse.checked(await merchantResp.json());
-
- if (response.reserve_sigs.length !== tipRecord.planchets.length) {
- throw Error("number of tip responses does not match requested planchets");
- }
-
- const planchets: PlanchetRecord[] = [];
-
- for (let i = 0; i < tipRecord.planchets.length; i++) {
- const tipPlanchet = tipRecord.planchets[i];
- const planchet: PlanchetRecord = {
- blindingKey: tipPlanchet.blindingKey,
- coinEv: tipPlanchet.coinEv,
- coinPriv: tipPlanchet.coinPriv,
- coinPub: tipPlanchet.coinPub,
- coinValue: tipPlanchet.coinValue,
- denomPub: tipPlanchet.denomPub,
- denomPubHash: tipPlanchet.denomPubHash,
- reservePub: response.reserve_pub,
- withdrawSig: response.reserve_sigs[i].reserve_sig,
- isFromTip: true,
- };
- planchets.push(planchet);
- }
-
- const withdrawalSessionId = encodeCrock(getRandomBytes(32));
-
- const withdrawalSession: WithdrawalSessionRecord = {
- denoms: planchets.map((x) => x.denomPub),
- exchangeBaseUrl: tipRecord.exchangeUrl,
- planchets: planchets,
- source: {
- type: "tip",
- tipId: tipRecord.tipId,
- },
- startTimestamp: getTimestampNow(),
- withdrawSessionId: withdrawalSessionId,
- rawWithdrawalAmount: tipRecord.amount,
- withdrawn: planchets.map((x) => false),
- totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
- lastCoinErrors: planchets.map((x) => undefined),
- retryInfo: initRetryInfo(),
- finishTimestamp: undefined,
- lastError: undefined,
- };
-
-
- await runWithWriteTransaction(ws.db, [Stores.tips, Stores.withdrawalSession], async (tx) => {
- const tr = await tx.get(Stores.tips, tipId);
- if (!tr) {
- return;
- }
- if (tr.pickedUp) {
- return;
- }
- tr.pickedUp = true;
- tr.retryInfo = initRetryInfo(false);
-
- await tx.put(Stores.tips, tr);
- await tx.put(Stores.withdrawalSession, withdrawalSession);
- });
-
- await processWithdrawSession(ws, withdrawalSessionId);
-
- return;
-}
-
-export async function acceptTip(
- ws: InternalWalletState,
- tipId: string,
-): Promise<void> {
- const tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
- if (!tipRecord) {
- console.log("tip not found");
- return;
- }
-
- tipRecord.accepted = true;
- await oneShotPut(ws.db, Stores.tips, tipRecord);
-
- await processTip(ws, tipId);
- return;
-}
diff --git a/src/wallet-impl/withdraw.ts b/src/wallet-impl/withdraw.ts
deleted file mode 100644
index d8b2b599c..000000000
--- a/src/wallet-impl/withdraw.ts
+++ /dev/null
@@ -1,699 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { AmountJson } from "../util/amounts";
-import {
- DenominationRecord,
- Stores,
- DenominationStatus,
- CoinStatus,
- CoinRecord,
- PlanchetRecord,
- initRetryInfo,
- updateRetryInfoTimeout,
-} from "../dbTypes";
-import * as Amounts from "../util/amounts";
-import {
- getTimestampNow,
- AcceptWithdrawalResponse,
- BankWithdrawDetails,
- ExchangeWithdrawDetails,
- WithdrawDetails,
- OperationError,
- NotificationType,
-} from "../walletTypes";
-import { WithdrawOperationStatusResponse } from "../talerTypes";
-import { InternalWalletState } from "./state";
-import { parseWithdrawUri } from "../util/taleruri";
-import { Logger } from "../util/logging";
-import {
- oneShotGet,
- oneShotPut,
- oneShotIterIndex,
- oneShotGetIndexed,
- runWithWriteTransaction,
- oneShotMutate,
-} from "../util/query";
-import {
- updateExchangeFromUrl,
- getExchangePaytoUri,
- getExchangeTrust,
-} from "./exchanges";
-import { createReserve, processReserveBankStatus } from "./reserves";
-import { WALLET_PROTOCOL_VERSION } from "../wallet";
-
-import * as LibtoolVersion from "../util/libtoolVersion";
-import { guardOperationException } from "./errors";
-
-const logger = new Logger("withdraw.ts");
-
-function isWithdrawableDenom(d: DenominationRecord) {
- const now = getTimestampNow();
- const started = now.t_ms >= d.stampStart.t_ms;
- const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
- return started && stillOkay;
-}
-
-/**
- * Get a list of denominations (with repetitions possible)
- * whose total value is as close as possible to the available
- * amount, but never larger.
- */
-export function getWithdrawDenomList(
- amountAvailable: AmountJson,
- denoms: DenominationRecord[],
-): DenominationRecord[] {
- let remaining = Amounts.copy(amountAvailable);
- const ds: DenominationRecord[] = [];
-
- denoms = denoms.filter(isWithdrawableDenom);
- denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
-
- // This is an arbitrary number of coins
- // we can withdraw in one go. It's not clear if this limit
- // is useful ...
- for (let i = 0; i < 1000; i++) {
- let found = false;
- for (const d of denoms) {
- const cost = Amounts.add(d.value, d.feeWithdraw).amount;
- if (Amounts.cmp(remaining, cost) < 0) {
- continue;
- }
- found = true;
- remaining = Amounts.sub(remaining, cost).amount;
- ds.push(d);
- break;
- }
- if (!found) {
- break;
- }
- }
- return ds;
-}
-
-/**
- * Get information about a withdrawal from
- * a taler://withdraw URI by asking the bank.
- */
-async function getBankWithdrawalInfo(
- ws: InternalWalletState,
- talerWithdrawUri: string,
-): Promise<BankWithdrawDetails> {
- const uriResult = parseWithdrawUri(talerWithdrawUri);
- if (!uriResult) {
- throw Error("can't parse URL");
- }
- const resp = await ws.http.get(uriResult.statusUrl);
- if (resp.status !== 200) {
- throw Error(`unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`);
- }
- const respJson = await resp.json();
- console.log("resp:", respJson);
- const status = WithdrawOperationStatusResponse.checked(respJson);
- return {
- amount: Amounts.parseOrThrow(status.amount),
- confirmTransferUrl: status.confirm_transfer_url,
- extractedStatusUrl: uriResult.statusUrl,
- selectionDone: status.selection_done,
- senderWire: status.sender_wire,
- suggestedExchange: status.suggested_exchange,
- transferDone: status.transfer_done,
- wireTypes: status.wire_types,
- };
-}
-
-export async function acceptWithdrawal(
- ws: InternalWalletState,
- talerWithdrawUri: string,
- selectedExchange: string,
-): Promise<AcceptWithdrawalResponse> {
- const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri);
- const exchangeWire = await getExchangePaytoUri(
- ws,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
- const reserve = await createReserve(ws, {
- amount: withdrawInfo.amount,
- bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
- exchange: selectedExchange,
- senderWire: withdrawInfo.senderWire,
- exchangeWire: exchangeWire,
- });
- // We do this here, as the reserve should be registered before we return,
- // so that we can redirect the user to the bank's status page.
- await processReserveBankStatus(ws, reserve.reservePub);
- console.log("acceptWithdrawal: returning");
- return {
- reservePub: reserve.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- };
-}
-
-async function getPossibleDenoms(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<DenominationRecord[]> {
- return await oneShotIterIndex(
- ws.db,
- Stores.denominations.exchangeBaseUrlIndex,
- exchangeBaseUrl,
- ).filter(d => {
- return (
- d.status === DenominationStatus.Unverified ||
- d.status === DenominationStatus.VerifiedGood
- );
- });
-}
-
-/**
- * Given a planchet, withdraw a coin from the exchange.
- */
-async function processPlanchet(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- coinIdx: number,
-): Promise<void> {
- const withdrawalSession = await oneShotGet(
- ws.db,
- Stores.withdrawalSession,
- withdrawalSessionId,
- );
- if (!withdrawalSession) {
- return;
- }
- if (withdrawalSession.withdrawn[coinIdx]) {
- return;
- }
- if (withdrawalSession.source.type === "reserve") {
- }
- const planchet = withdrawalSession.planchets[coinIdx];
- if (!planchet) {
- console.log("processPlanchet: planchet not found");
- return;
- }
- const exchange = await oneShotGet(
- ws.db,
- Stores.exchanges,
- withdrawalSession.exchangeBaseUrl,
- );
- if (!exchange) {
- console.error("db inconsistent: exchange for planchet not found");
- return;
- }
-
- const denom = await oneShotGet(ws.db, Stores.denominations, [
- withdrawalSession.exchangeBaseUrl,
- planchet.denomPub,
- ]);
-
- if (!denom) {
- console.error("db inconsistent: denom for planchet not found");
- return;
- }
-
- const wd: any = {};
- wd.denom_pub_hash = planchet.denomPubHash;
- wd.reserve_pub = planchet.reservePub;
- wd.reserve_sig = planchet.withdrawSig;
- wd.coin_ev = planchet.coinEv;
- const reqUrl = new URL("reserve/withdraw", exchange.baseUrl).href;
- const resp = await ws.http.postJson(reqUrl, wd);
- if (resp.status !== 200) {
- throw Error(`unexpected status ${resp.status} for withdraw`);
- }
-
- const r = await resp.json();
-
- const denomSig = await ws.cryptoApi.rsaUnblind(
- r.ev_sig,
- planchet.blindingKey,
- planchet.denomPub,
- );
-
-
- const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, planchet.denomPub);
- if (!isValid) {
- throw Error("invalid RSA signature by the exchange");
- }
-
- const coin: CoinRecord = {
- blindingKey: planchet.blindingKey,
- coinPriv: planchet.coinPriv,
- coinPub: planchet.coinPub,
- currentAmount: planchet.coinValue,
- denomPub: planchet.denomPub,
- denomPubHash: planchet.denomPubHash,
- denomSig,
- exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
- reservePub: planchet.reservePub,
- status: CoinStatus.Fresh,
- coinIndex: coinIdx,
- withdrawSessionId: withdrawalSessionId,
- };
-
- let withdrawSessionFinished = false;
- let reserveDepleted = false;
-
- const success = await runWithWriteTransaction(
- ws.db,
- [Stores.coins, Stores.withdrawalSession, Stores.reserves],
- async tx => {
- const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
- if (!ws) {
- return false;
- }
- if (ws.withdrawn[coinIdx]) {
- // Already withdrawn
- return false;
- }
- ws.withdrawn[coinIdx] = true;
- ws.lastCoinErrors[coinIdx] = undefined;
- let numDone = 0;
- for (let i = 0; i < ws.withdrawn.length; i++) {
- if (ws.withdrawn[i]) {
- numDone++;
- }
- }
- if (numDone === ws.denoms.length) {
- ws.finishTimestamp = getTimestampNow();
- ws.lastError = undefined;
- ws.retryInfo = initRetryInfo(false);
- withdrawSessionFinished = true;
- }
- await tx.put(Stores.withdrawalSession, ws);
- if (!planchet.isFromTip) {
- const r = await tx.get(Stores.reserves, planchet.reservePub);
- if (r) {
- r.withdrawCompletedAmount = Amounts.add(
- r.withdrawCompletedAmount,
- Amounts.add(denom.value, denom.feeWithdraw).amount,
- ).amount;
- if (Amounts.cmp(r.withdrawCompletedAmount, r.withdrawAllocatedAmount) == 0) {
- reserveDepleted = true;
- }
- await tx.put(Stores.reserves, r);
- }
- }
- await tx.add(Stores.coins, coin);
- return true;
- },
- );
-
- if (success) {
- ws.notify( {
- type: NotificationType.CoinWithdrawn,
- } );
- }
-
- if (withdrawSessionFinished) {
- ws.notify({
- type: NotificationType.WithdrawSessionFinished,
- withdrawSessionId: withdrawalSessionId,
- });
- }
-
- if (reserveDepleted && withdrawalSession.source.type === "reserve") {
- ws.notify({
- type: NotificationType.ReserveDepleted,
- reservePub: withdrawalSession.source.reservePub,
- });
- }
-}
-
-/**
- * Get a list of denominations to withdraw from the given exchange for the
- * given amount, making sure that all denominations' signatures are verified.
- *
- * Writes to the DB in order to record the result from verifying
- * denominations.
- */
-export async function getVerifiedWithdrawDenomList(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- amount: AmountJson,
-): Promise<DenominationRecord[]> {
- const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
- if (!exchange) {
- console.log("exchange not found");
- throw Error(`exchange ${exchangeBaseUrl} not found`);
- }
- const exchangeDetails = exchange.details;
- if (!exchangeDetails) {
- console.log("exchange details not available");
- throw Error(`exchange ${exchangeBaseUrl} details not available`);
- }
-
- console.log("getting possible denoms");
-
- const possibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl);
-
- console.log("got possible denoms");
-
- let allValid = false;
-
- let selectedDenoms: DenominationRecord[];
-
- do {
- allValid = true;
- const nextPossibleDenoms = [];
- selectedDenoms = getWithdrawDenomList(amount, possibleDenoms);
- console.log("got withdraw denom list");
- for (const denom of selectedDenoms || []) {
- if (denom.status === DenominationStatus.Unverified) {
- console.log(
- "checking validity",
- denom,
- exchangeDetails.masterPublicKey,
- );
- const valid = await ws.cryptoApi.isValidDenom(
- denom,
- exchangeDetails.masterPublicKey,
- );
- console.log("done checking validity");
- if (!valid) {
- denom.status = DenominationStatus.VerifiedBad;
- allValid = false;
- } else {
- denom.status = DenominationStatus.VerifiedGood;
- nextPossibleDenoms.push(denom);
- }
- await oneShotPut(ws.db, Stores.denominations, denom);
- } else {
- nextPossibleDenoms.push(denom);
- }
- }
- } while (selectedDenoms.length > 0 && !allValid);
-
- console.log("returning denoms");
-
- return selectedDenoms;
-}
-
-async function makePlanchet(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- coinIndex: number,
-): Promise<void> {
- const withdrawalSession = await oneShotGet(
- ws.db,
- Stores.withdrawalSession,
- withdrawalSessionId,
- );
- if (!withdrawalSession) {
- return;
- }
- const src = withdrawalSession.source;
- if (src.type !== "reserve") {
- throw Error("invalid state");
- }
- const reserve = await oneShotGet(ws.db, Stores.reserves, src.reservePub);
- if (!reserve) {
- return;
- }
- const denom = await oneShotGet(ws.db, Stores.denominations, [
- withdrawalSession.exchangeBaseUrl,
- withdrawalSession.denoms[coinIndex],
- ]);
- if (!denom) {
- return;
- }
- const r = await ws.cryptoApi.createPlanchet({
- denomPub: denom.denomPub,
- feeWithdraw: denom.feeWithdraw,
- reservePriv: reserve.reservePriv,
- reservePub: reserve.reservePub,
- value: denom.value,
- });
- const newPlanchet: PlanchetRecord = {
- blindingKey: r.blindingKey,
- coinEv: r.coinEv,
- coinPriv: r.coinPriv,
- coinPub: r.coinPub,
- coinValue: r.coinValue,
- denomPub: r.denomPub,
- denomPubHash: r.denomPubHash,
- isFromTip: false,
- reservePub: r.reservePub,
- withdrawSig: r.withdrawSig,
- };
- await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => {
- const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
- if (!myWs) {
- return;
- }
- if (myWs.planchets[coinIndex]) {
- return;
- }
- myWs.planchets[coinIndex] = newPlanchet;
- await tx.put(Stores.withdrawalSession, myWs);
- });
-}
-
-async function processWithdrawCoin(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- coinIndex: number,
-) {
- logger.trace("starting withdraw for coin", coinIndex);
- const withdrawalSession = await oneShotGet(
- ws.db,
- Stores.withdrawalSession,
- withdrawalSessionId,
- );
- if (!withdrawalSession) {
- console.log("ws doesn't exist");
- return;
- }
-
- const coin = await oneShotGetIndexed(
- ws.db,
- Stores.coins.byWithdrawalWithIdx,
- [withdrawalSessionId, coinIndex],
- );
-
- if (coin) {
- console.log("coin already exists");
- return;
- }
-
- if (!withdrawalSession.planchets[coinIndex]) {
- const key = `${withdrawalSessionId}-${coinIndex}`;
- await ws.memoMakePlanchet.memo(key, async () => {
- logger.trace("creating planchet for coin", coinIndex);
- return makePlanchet(ws, withdrawalSessionId, coinIndex);
- });
- }
- await processPlanchet(ws, withdrawalSessionId, coinIndex);
-}
-
-async function incrementWithdrawalRetry(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- err: OperationError | undefined,
-): Promise<void> {
- await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => {
- const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
- if (!wsr) {
- return;
- }
- if (!wsr.retryInfo) {
- return;
- }
- wsr.retryInfo.retryCounter++;
- updateRetryInfoTimeout(wsr.retryInfo);
- wsr.lastError = err;
- await tx.put(Stores.withdrawalSession, wsr);
- });
- ws.notify({ type: NotificationType.WithdrawOperationError });
-}
-
-export async function processWithdrawSession(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- forceNow: boolean = false,
-): Promise<void> {
- const onOpErr = (e: OperationError) =>
- incrementWithdrawalRetry(ws, withdrawalSessionId, e);
- await guardOperationException(
- () => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow),
- onOpErr,
- );
-}
-
-async function resetWithdrawSessionRetry(
- ws: InternalWalletState,
- withdrawalSessionId: string,
-) {
- await oneShotMutate(ws.db, Stores.withdrawalSession, withdrawalSessionId, (x) => {
- if (x.retryInfo.active) {
- x.retryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processWithdrawSessionImpl(
- ws: InternalWalletState,
- withdrawalSessionId: string,
- forceNow: boolean,
-): Promise<void> {
- logger.trace("processing withdraw session", withdrawalSessionId);
- if (forceNow) {
- await resetWithdrawSessionRetry(ws, withdrawalSessionId);
- }
- const withdrawalSession = await oneShotGet(
- ws.db,
- Stores.withdrawalSession,
- withdrawalSessionId,
- );
- if (!withdrawalSession) {
- logger.trace("withdraw session doesn't exist");
- return;
- }
-
- const ps = withdrawalSession.denoms.map((d, i) =>
- processWithdrawCoin(ws, withdrawalSessionId, i),
- );
- await Promise.all(ps);
- return;
-}
-
-export async function getExchangeWithdrawalInfo(
- ws: InternalWalletState,
- 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 selectedDenoms = await getVerifiedWithdrawDenomList(
- ws,
- baseUrl,
- amount,
- );
- let acc = Amounts.getZero(amount.currency);
- for (const d of selectedDenoms) {
- acc = Amounts.add(acc, d.feeWithdraw).amount;
- }
- const actualCoinCost = selectedDenoms
- .map((d: DenominationRecord) => Amounts.add(d.value, d.feeWithdraw).amount)
- .reduce((a, b) => Amounts.add(a, b).amount);
-
- const exchangeWireAccounts: string[] = [];
- for (let account of exchangeWireInfo.accounts) {
- exchangeWireAccounts.push(account.url);
- }
-
- const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
-
- let earliestDepositExpiration = selectedDenoms[0].stampExpireDeposit;
- for (let i = 1; i < selectedDenoms.length; i++) {
- const expireDeposit = selectedDenoms[i].stampExpireDeposit;
- if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) {
- earliestDepositExpiration = expireDeposit;
- }
- }
-
- const possibleDenoms = await oneShotIterIndex(
- ws.db,
- Stores.denominations.exchangeBaseUrlIndex,
- baseUrl,
- ).filter(d => d.isOffered);
-
- const trustedAuditorPubs = [];
- const currencyRecord = await oneShotGet(
- ws.db,
- Stores.currencies,
- amount.currency,
- );
- if (currencyRecord) {
- trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub));
- }
-
- let versionMatch;
- if (exchangeDetails.protocolVersion) {
- versionMatch = LibtoolVersion.compare(
- WALLET_PROTOCOL_VERSION,
- exchangeDetails.protocolVersion,
- );
-
- if (
- versionMatch &&
- !versionMatch.compatible &&
- versionMatch.currentCmp === -1
- ) {
- console.warn(
- `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
- );
- }
- }
-
- let tosAccepted = false;
-
- if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
- if (exchangeInfo.termsOfServiceAcceptedEtag == exchangeInfo.termsOfServiceLastEtag) {
- tosAccepted = true;
- }
- }
-
- const ret: ExchangeWithdrawDetails = {
- earliestDepositExpiration,
- exchangeInfo,
- exchangeWireAccounts,
- exchangeVersion: exchangeDetails.protocolVersion || "unknown",
- isAudited,
- isTrusted,
- numOfferedDenoms: possibleDenoms.length,
- overhead: Amounts.sub(amount, actualCoinCost).amount,
- selectedDenoms,
- trustedAuditorPubs,
- versionMatch,
- walletVersion: WALLET_PROTOCOL_VERSION,
- wireFees: exchangeWireInfo,
- withdrawFee: acc,
- termsOfServiceAccepted: tosAccepted,
- };
- return ret;
-}
-
-export async function getWithdrawDetailsForUri(
- ws: InternalWalletState,
- talerWithdrawUri: string,
- maybeSelectedExchange?: string,
-): Promise<WithdrawDetails> {
- const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
- let rci: ExchangeWithdrawDetails | undefined = undefined;
- if (maybeSelectedExchange) {
- rci = await getExchangeWithdrawalInfo(
- ws,
- maybeSelectedExchange,
- info.amount,
- );
- }
- return {
- bankWithdrawDetails: info,
- exchangeWithdrawDetails: rci,
- };
-}