aboutsummaryrefslogtreecommitdiff
path: root/lib/wallet/wallet.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-11-13 23:30:18 +0100
committerFlorian Dold <florian.dold@gmail.com>2016-11-13 23:31:17 +0100
commitf3fb8be7db6de87dae40d41bd5597a735c800ca1 (patch)
tree1a061db04de8f5bb5a6b697fa56a9948f67fac2f /lib/wallet/wallet.ts
parent200d83c3886149ebb3f018530302079e12a81f6b (diff)
restructuring
Diffstat (limited to 'lib/wallet/wallet.ts')
-rw-r--r--lib/wallet/wallet.ts1657
1 files changed, 0 insertions, 1657 deletions
diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts
deleted file mode 100644
index 9fb6e5a27..000000000
--- a/lib/wallet/wallet.ts
+++ /dev/null
@@ -1,1657 +0,0 @@
-/*
- This file is part of TALER
- (C) 2015 GNUnet e.V.
-
- 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.
-
- 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
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * High-level wallet operations that should be indepentent from the underlying
- * browser extension interface.
- * @module Wallet
- * @author Florian Dold
- */
-
-import {
- AmountJson,
- Amounts,
- CheckRepurchaseResult,
- Coin,
- CoinPaySig,
- Contract,
- CreateReserveResponse,
- Denomination,
- ExchangeHandle,
- IExchangeInfo,
- Notifier,
- PayCoinInfo,
- PreCoin,
- RefreshSession,
- ReserveCreationInfo,
- ReserveRecord,
- WalletBalance,
- WalletBalanceEntry,
- WireInfo,
-} from "./types";
-import {
- HttpRequestLibrary,
- HttpResponse,
- RequestException,
-} from "./http";
-import {
- AbortTransaction,
- Index,
- JoinResult,
- QueryRoot,
- Store,
-} from "./query";
-import {Checkable} from "./checkable";
-import {
- amountToPretty,
- canonicalizeBaseUrl,
- canonicalJson,
- deepEquals,
- flatMap,
- getTalerStampSec,
-} from "./helpers";
-import {CryptoApi} from "./cryptoApi";
-
-"use strict";
-
-export interface CoinWithDenom {
- coin: Coin;
- denom: Denomination;
-}
-
-
-@Checkable.Class
-export class KeysJson {
- @Checkable.List(Checkable.Value(Denomination))
- denoms: Denomination[];
-
- @Checkable.String
- master_public_key: string;
-
- @Checkable.Any
- auditors: any[];
-
- @Checkable.String
- list_issue_date: string;
-
- @Checkable.Any
- signkeys: any;
-
- @Checkable.String
- eddsa_pub: string;
-
- @Checkable.String
- eddsa_sig: string;
-
- static checked: (obj: any) => KeysJson;
-}
-
-
-@Checkable.Class
-export class CreateReserveRequest {
- /**
- * The initial amount for the reserve.
- */
- @Checkable.Value(AmountJson)
- amount: AmountJson;
-
- /**
- * Exchange URL where the bank should create the reserve.
- */
- @Checkable.String
- exchange: string;
-
- static checked: (obj: any) => CreateReserveRequest;
-}
-
-
-@Checkable.Class
-export class ConfirmReserveRequest {
- /**
- * Public key of then reserve that should be marked
- * as confirmed.
- */
- @Checkable.String
- reservePub: string;
-
- static checked: (obj: any) => ConfirmReserveRequest;
-}
-
-
-@Checkable.Class
-export class Offer {
- @Checkable.Value(Contract)
- contract: Contract;
-
- @Checkable.String
- merchant_sig: string;
-
- @Checkable.String
- H_contract: string;
-
- @Checkable.Number
- offer_time: number;
-
- /**
- * Serial ID when the offer is stored in the wallet DB.
- */
- @Checkable.Optional(Checkable.Number)
- id?: number;
-
- static checked: (obj: any) => Offer;
-}
-
-export interface HistoryRecord {
- type: string;
- timestamp: number;
- subjectId?: string;
- detail: any;
- level: HistoryLevel;
-}
-
-
-interface ExchangeCoins {
- [exchangeUrl: string]: CoinWithDenom[];
-}
-
-interface PayReq {
- amount: AmountJson;
- coins: CoinPaySig[];
- H_contract: string;
- max_fee: AmountJson;
- merchant_sig: string;
- exchange: string;
- refund_deadline: string;
- timestamp: string;
- transaction_id: number;
- pay_deadline: string;
- /**
- * Merchant instance identifier that should receive the
- * payment, if applicable.
- */
- instance?: string;
-}
-
-interface Transaction {
- contractHash: string;
- contract: Contract;
- payReq: PayReq;
- merchantSig: string;
-
- /**
- * The transaction isn't active anymore, it's either successfully paid
- * or refunded/aborted.
- */
- finished: boolean;
-}
-
-export enum HistoryLevel {
- Trace = 1,
- Developer = 2,
- Expert = 3,
- User = 4,
-}
-
-
-export interface Badge {
- setText(s: string): void;
- setColor(c: string): void;
- startBusy(): void;
- stopBusy(): void;
-}
-
-
-function setTimeout(f: any, t: number) {
- return chrome.extension.getBackgroundPage().setTimeout(f, t);
-}
-
-
-function isWithdrawableDenom(d: Denomination) {
- const now_sec = (new Date).getTime() / 1000;
- const stamp_withdraw_sec = getTalerStampSec(d.stamp_expire_withdraw);
- const stamp_start_sec = getTalerStampSec(d.stamp_start);
- // Withdraw if still possible to withdraw within a minute
- if ((stamp_withdraw_sec + 60 > now_sec) && (now_sec >= stamp_start_sec)) {
- return true;
- }
- return false;
-}
-
-
-/**
- * Result of updating exisiting information
- * about an exchange with a new '/keys' response.
- */
-interface KeyUpdateInfo {
- updatedExchangeInfo: IExchangeInfo;
- addedDenominations: Denomination[];
- removedDenominations: Denomination[];
-}
-
-
-/**
- * Get a list of denominations (with repetitions possible)
- * whose total value is as close as possible to the available
- * amount, but never larger.
- */
-function getWithdrawDenomList(amountAvailable: AmountJson,
- denoms: Denomination[]): Denomination[] {
- let remaining = Amounts.copy(amountAvailable);
- const ds: Denomination[] = [];
-
- 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 (let d of denoms) {
- let cost = Amounts.add(d.value, d.fee_withdraw).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;
-}
-
-
-export namespace Stores {
- class ExchangeStore extends Store<IExchangeInfo> {
- constructor() {
- super("exchanges", {keyPath: "baseUrl"});
- }
-
- pubKeyIndex = new Index<string,IExchangeInfo>(this, "pubKey", "masterPublicKey");
- }
-
- class CoinsStore extends Store<Coin> {
- constructor() {
- super("coins", {keyPath: "coinPub"});
- }
-
- exchangeBaseUrlIndex = new Index<string,Coin>(this, "exchangeBaseUrl", "exchangeBaseUrl");
- }
-
- class HistoryStore extends Store<HistoryRecord> {
- constructor() {
- super("history", {
- keyPath: "id",
- autoIncrement: true
- });
- }
-
- timestampIndex = new Index<number,HistoryRecord>(this, "timestamp", "timestamp");
- }
-
- class OffersStore extends Store<Offer> {
- constructor() {
- super("offers", {
- keyPath: "id",
- autoIncrement: true
- });
- }
- }
-
- class TransactionsStore extends Store<Transaction> {
- constructor() {
- super("transactions", {keyPath: "contractHash"});
- }
-
- repurchaseIndex = new Index<[string,string],Transaction>(this, "repurchase", [
- "contract.merchant_pub",
- "contract.repurchase_correlation_id"
- ]);
- }
-
- export let exchanges: ExchangeStore = new ExchangeStore();
- export let transactions: TransactionsStore = new TransactionsStore();
- export let reserves: Store<ReserveRecord> = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"});
- export let coins: CoinsStore = new CoinsStore();
- export let refresh: Store<RefreshSession> = new Store<RefreshSession>("refresh", {keyPath: "meltCoinPub"});
- export let history: HistoryStore = new HistoryStore();
- export let offers: OffersStore = new OffersStore();
- export let precoins: Store<PreCoin> = new Store<PreCoin>("precoins", {keyPath: "coinPub"});
-}
-
-
-export class Wallet {
- private db: IDBDatabase;
- private http: HttpRequestLibrary;
- private badge: Badge;
- private notifier: Notifier;
- public cryptoApi: CryptoApi;
-
- /**
- * Set of identifiers for running operations.
- */
- private runningOperations: Set<string> = new Set();
-
- q(): QueryRoot {
- return new QueryRoot(this.db);
- }
-
- constructor(db: IDBDatabase,
- http: HttpRequestLibrary,
- badge: Badge,
- notifier: Notifier) {
- this.db = db;
- this.http = http;
- this.badge = badge;
- this.notifier = notifier;
- this.cryptoApi = new CryptoApi();
-
- this.resumePendingFromDb();
- }
-
-
- private startOperation(operationId: string) {
- this.runningOperations.add(operationId);
- this.badge.startBusy();
- }
-
- private stopOperation(operationId: string) {
- this.runningOperations.delete(operationId);
- if (this.runningOperations.size == 0) {
- this.badge.stopBusy();
- }
- }
-
- async updateExchanges(): Promise<void> {
- console.log("updating exchanges");
-
- let exchangesUrls = await this.q()
- .iter(Stores.exchanges)
- .map((e) => e.baseUrl)
- .toArray();
-
- for (let url of exchangesUrls) {
- this.updateExchangeFromUrl(url)
- .catch((e) => {
- console.error("updating exchange failed", e);
- });
- }
- }
-
- /**
- * Resume various pending operations that are pending
- * by looking at the database.
- */
- private resumePendingFromDb(): void {
- console.log("resuming pending operations from db");
-
- this.q()
- .iter(Stores.reserves)
- .reduce((reserve) => {
- console.log("resuming reserve", reserve.reserve_pub);
- this.processReserve(reserve);
- });
-
- this.q()
- .iter(Stores.precoins)
- .reduce((preCoin) => {
- console.log("resuming precoin");
- this.processPreCoin(preCoin);
- });
-
- this.q()
- .iter(Stores.refresh)
- .reduce((r: RefreshSession) => {
- this.continueRefreshSession(r);
- });
-
- // FIXME: optimize via index
- this.q()
- .iter(Stores.coins)
- .reduce((c: Coin) => {
- if (c.dirty && !c.transactionPending) {
- this.refresh(c.coinPub);
- }
- });
- }
-
-
- /**
- * Get exchanges and associated coins that are still spendable,
- * but only if the sum the coins' remaining value exceeds the payment amount.
- */
- private async getPossibleExchangeCoins(paymentAmount: AmountJson,
- depositFeeLimit: AmountJson,
- allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> {
- // Mapping from exchange base URL to list of coins together with their
- // denomination
- let m: ExchangeCoins = {};
-
- let x: number;
-
- function storeExchangeCoin(mc: JoinResult<IExchangeInfo, Coin>,
- url: string) {
- let exchange: IExchangeInfo = mc.left;
- console.log("got coin for exchange", url);
- let coin: Coin = mc.right;
- if (coin.suspended) {
- console.log("skipping suspended coin",
- coin.denomPub,
- "from exchange",
- exchange.baseUrl);
- return;
- }
- let denom = exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub);
- if (!denom) {
- console.warn("denom not found (database inconsistent)");
- return;
- }
- if (denom.value.currency !== paymentAmount.currency) {
- console.warn("same pubkey for different currencies");
- return;
- }
- let cd = {coin, denom};
- let x = m[url];
- if (!x) {
- m[url] = [cd];
- } else {
- x.push(cd);
- }
- }
-
- // Make sure that we don't look up coins
- // for the same URL twice ...
- let handledExchanges = new Set();
-
- let ps = flatMap(allowedExchanges, (info: ExchangeHandle) => {
- if (handledExchanges.has(info.url)) {
- return [];
- }
- handledExchanges.add(info.url);
- console.log("Checking for merchant's exchange", JSON.stringify(info));
- return [
- this.q()
- .iterIndex(Stores.exchanges.pubKeyIndex, info.master_pub)
- .indexJoin(Stores.coins.exchangeBaseUrlIndex,
- (exchange) => exchange.baseUrl)
- .reduce((x) => storeExchangeCoin(x, info.url))
- ];
- });
-
- await Promise.all(ps);
-
- let ret: ExchangeCoins = {};
-
- if (Object.keys(m).length == 0) {
- console.log("not suitable exchanges found");
- }
-
- console.log("exchange coins:");
- console.dir(m);
-
- // We try to find the first exchange where we have
- // enough coins to cover the paymentAmount with fees
- // under depositFeeLimit
-
- nextExchange:
- for (let key in m) {
- let coins = m[key];
- // Sort by ascending deposit fee
- coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit,
- o2.denom.fee_deposit));
- let maxFee = Amounts.copy(depositFeeLimit);
- let minAmount = Amounts.copy(paymentAmount);
- let accFee = Amounts.copy(coins[0].denom.fee_deposit);
- let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency);
- let usableCoins: CoinWithDenom[] = [];
- nextCoin:
- for (let i = 0; i < coins.length; i++) {
- let coinAmount = Amounts.copy(coins[i].coin.currentAmount);
- let coinFee = coins[i].denom.fee_deposit;
- if (Amounts.cmp(coinAmount, coinFee) <= 0) {
- continue nextCoin;
- }
- accFee = Amounts.add(accFee, coinFee).amount;
- accAmount = Amounts.add(accAmount, coinAmount).amount;
- if (Amounts.cmp(accFee, maxFee) >= 0) {
- // FIXME: if the fees are too high, we have
- // to cover them ourselves ....
- console.log("too much fees");
- continue nextExchange;
- }
- usableCoins.push(coins[i]);
- if (Amounts.cmp(accAmount, minAmount) >= 0) {
- ret[key] = usableCoins;
- continue nextExchange;
- }
- }
- }
- return ret;
- }
-
-
- /**
- * Record all information that is necessary to
- * pay for a contract in the wallet's database.
- */
- private async recordConfirmPay(offer: Offer,
- payCoinInfo: PayCoinInfo,
- chosenExchange: string): Promise<void> {
- let payReq: PayReq = {
- amount: offer.contract.amount,
- coins: payCoinInfo.map((x) => x.sig),
- H_contract: offer.H_contract,
- max_fee: offer.contract.max_fee,
- merchant_sig: offer.merchant_sig,
- exchange: URI(chosenExchange).href(),
- refund_deadline: offer.contract.refund_deadline,
- pay_deadline: offer.contract.pay_deadline,
- timestamp: offer.contract.timestamp,
- transaction_id: offer.contract.transaction_id,
- instance: offer.contract.merchant.instance
- };
- let t: Transaction = {
- contractHash: offer.H_contract,
- contract: offer.contract,
- payReq: payReq,
- merchantSig: offer.merchant_sig,
- finished: false,
- };
-
- let historyEntry: HistoryRecord = {
- type: "pay",
- timestamp: (new Date).getTime(),
- subjectId: `contract-${offer.H_contract}`,
- detail: {
- merchantName: offer.contract.merchant.name,
- amount: offer.contract.amount,
- contractHash: offer.H_contract,
- fulfillmentUrl: offer.contract.fulfillment_url,
- },
- level: HistoryLevel.User
- };
-
- await this.q()
- .put(Stores.transactions, t)
- .put(Stores.history, historyEntry)
- .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin))
- .finish();
-
- this.notifier.notify();
- }
-
-
- async putHistory(historyEntry: HistoryRecord): Promise<void> {
- await this.q().put(Stores.history, historyEntry).finish();
- this.notifier.notify();
- }
-
-
- async saveOffer(offer: Offer): Promise<number> {
- console.log(`saving offer in wallet.ts`);
- let id = await this.q().putWithResult(Stores.offers, offer);
- this.notifier.notify();
- console.log(`saved offer with id ${id}`);
- if (typeof id !== "number") {
- throw Error("db schema wrong");
- }
- return id;
- }
-
-
- /**
- * Add a contract to the wallet and sign coins,
- * but do not send them yet.
- */
- async confirmPay(offer: Offer): Promise<any> {
- console.log("executing confirmPay");
-
- let transaction = await this.q().get(Stores.transactions, offer.H_contract);
-
- if (transaction) {
- // Already payed ...
- return {};
- }
-
- let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
- offer.contract.max_fee,
- offer.contract.exchanges);
-
- if (Object.keys(mcs).length == 0) {
- console.log("not confirming payment, insufficient coins");
- return {
- error: "coins-insufficient",
- };
- }
- let exchangeUrl = Object.keys(mcs)[0];
-
- let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]);
- await this.recordConfirmPay(offer,
- ds,
- exchangeUrl);
- return {};
- }
-
-
- /**
- * Add a contract to the wallet and sign coins,
- * but do not send them yet.
- */
- async checkPay(offer: Offer): Promise<any> {
- // First check if we already payed for it.
- let transaction = await this.q().get(Stores.transactions, offer.H_contract);
- if (transaction) {
- return {isPayed: true};
- }
-
- // If not already payed, check if we could pay for it.
- let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
- offer.contract.max_fee,
- offer.contract.exchanges);
-
- if (Object.keys(mcs).length == 0) {
- console.log("not confirming payment, insufficient coins");
- return {
- error: "coins-insufficient",
- };
- }
- return {isPayed: false};
- }
-
-
- /**
- * Retrieve all necessary information for looking up the contract
- * with the given hash.
- */
- async executePayment(H_contract: string): Promise<any> {
- let t = await this.q().get<Transaction>(Stores.transactions, H_contract);
- if (!t) {
- return {
- success: false,
- contractFound: false,
- }
- }
- let resp = {
- success: true,
- payReq: t.payReq,
- contract: t.contract,
- };
- return resp;
- }
-
-
- /**
- * First fetch information requred to withdraw from the reserve,
- * then deplete the reserve, withdrawing coins until it is empty.
- */
- private async processReserve(reserveRecord: ReserveRecord,
- retryDelayMs: number = 250): Promise<void> {
- const opId = "reserve-" + reserveRecord.reserve_pub;
- this.startOperation(opId);
-
- try {
- let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
- let reserve = await this.updateReserve(reserveRecord.reserve_pub,
- exchange);
- let n = await this.depleteReserve(reserve, exchange);
-
- if (n != 0) {
- let depleted: HistoryRecord = {
- type: "depleted-reserve",
- subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
- timestamp: (new Date).getTime(),
- detail: {
- exchangeBaseUrl: reserveRecord.exchange_base_url,
- reservePub: reserveRecord.reserve_pub,
- requestedAmount: reserveRecord.requested_amount,
- currentAmount: reserveRecord.current_amount,
- },
- level: HistoryLevel.User
- };
- await this.q().put(Stores.history, depleted).finish();
- }
- } catch (e) {
- // random, exponential backoff truncated at 3 minutes
- let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(),
- 3000 * 60);
- console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`);
- setTimeout(() => this.processReserve(reserveRecord, nextDelay),
- retryDelayMs);
- } finally {
- this.stopOperation(opId);
- }
- }
-
-
- private async processPreCoin(preCoin: PreCoin,
- retryDelayMs = 100): Promise<void> {
-
- let exchange = await this.q().get(Stores.exchanges,
- preCoin.exchangeBaseUrl);
- if (!exchange) {
- console.error("db inconsistend: exchange for precoin not found");
- return;
- }
- let denom = exchange.all_denoms.find((d) => d.denom_pub == preCoin.denomPub);
- if (!denom) {
- console.error("db inconsistent: denom for precoin not found");
- return;
- }
-
- try {
- const coin = await this.withdrawExecute(preCoin);
-
- const mutateReserve = (r: ReserveRecord) => {
-
- console.log(`before committing coin: current ${amountToPretty(r.current_amount!)}, precoin: ${amountToPretty(
- r.precoin_amount)})}`);
-
- let x = Amounts.sub(r.precoin_amount,
- preCoin.coinValue,
- denom!.fee_withdraw);
- if (x.saturated) {
- console.error("database inconsistent");
- throw AbortTransaction;
- }
- r.precoin_amount = x.amount;
- return r;
- };
-
- let historyEntry: HistoryRecord = {
- type: "withdraw",
- timestamp: (new Date).getTime(),
- level: HistoryLevel.Expert,
- detail: {
- coinPub: coin.coinPub,
- }
- };
-
- await this.q()
- .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
- .delete("precoins", coin.coinPub)
- .add(Stores.coins, coin)
- .add(Stores.history, historyEntry)
- .finish();
-
- this.notifier.notify();
- } catch (e) {
- console.error("Failed to withdraw coin from precoin, retrying in",
- retryDelayMs,
- "ms", e);
- // exponential backoff truncated at one minute
- let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60);
- setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs),
- retryDelayMs);
- }
- }
-
-
- /**
- * Create a reserve, but do not flag it as confirmed yet.
- */
- async createReserve(req: CreateReserveRequest): Promise<CreateReserveResponse> {
- let keypair = await this.cryptoApi.createEddsaKeypair();
- const now = (new Date).getTime();
- const canonExchange = canonicalizeBaseUrl(req.exchange);
-
- const reserveRecord: ReserveRecord = {
- reserve_pub: keypair.pub,
- reserve_priv: keypair.priv,
- exchange_base_url: canonExchange,
- created: now,
- last_query: null,
- current_amount: null,
- requested_amount: req.amount,
- confirmed: false,
- precoin_amount: Amounts.getZero(req.amount.currency),
- };
-
- const historyEntry = {
- type: "create-reserve",
- level: HistoryLevel.Expert,
- timestamp: now,
- subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
- detail: {
- requestedAmount: req.amount,
- reservePub: reserveRecord.reserve_pub,
- }
- };
-
- await this.q()
- .put(Stores.reserves, reserveRecord)
- .put(Stores.history, historyEntry)
- .finish();
-
- let r: CreateReserveResponse = {
- exchange: canonExchange,
- reservePub: keypair.pub,
- };
- return r;
- }
-
-
- /**
- * Mark an existing reserve as confirmed. The wallet will start trying
- * to withdraw from that reserve. This may not immediately succeed,
- * since the exchange might not know about the reserve yet, even though the
- * bank confirmed its creation.
- *
- * A confirmed reserve should be shown to the user in the UI, while
- * an unconfirmed reserve should be hidden.
- */
- async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
- const now = (new Date).getTime();
- let reserve: ReserveRecord|undefined = await (
- this.q().get<ReserveRecord>(Stores.reserves,
- req.reservePub));
- if (!reserve) {
- console.error("Unable to confirm reserve, not found in DB");
- return;
- }
- console.log("reserve confirmed");
- const historyEntry: HistoryRecord = {
- type: "confirm-reserve",
- timestamp: now,
- subjectId: `reserve-progress-${reserve.reserve_pub}`,
- detail: {
- exchangeBaseUrl: reserve.exchange_base_url,
- reservePub: req.reservePub,
- requestedAmount: reserve.requested_amount,
- },
- level: HistoryLevel.User,
- };
- reserve.confirmed = true;
- await this.q()
- .put(Stores.reserves, reserve)
- .put(Stores.history, historyEntry)
- .finish();
- this.notifier.notify();
-
- this.processReserve(reserve);
- }
-
-
- private async withdrawExecute(pc: PreCoin): Promise<Coin> {
- let reserve = await this.q().get<ReserveRecord>(Stores.reserves,
- pc.reservePub);
-
- if (!reserve) {
- throw Error("db inconsistent");
- }
-
- let wd: any = {};
- wd.denom_pub = pc.denomPub;
- wd.reserve_pub = pc.reservePub;
- wd.reserve_sig = pc.withdrawSig;
- wd.coin_ev = pc.coinEv;
- let reqUrl = URI("reserve/withdraw").absoluteTo(reserve.exchange_base_url);
- let resp = await this.http.postJson(reqUrl, wd);
-
-
- if (resp.status != 200) {
- throw new RequestException({
- hint: "Withdrawal failed",
- status: resp.status
- });
- }
- let r = JSON.parse(resp.responseText);
- let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig,
- pc.blindingKey,
- pc.denomPub);
- let coin: Coin = {
- coinPub: pc.coinPub,
- coinPriv: pc.coinPriv,
- denomPub: pc.denomPub,
- denomSig: denomSig,
- currentAmount: pc.coinValue,
- exchangeBaseUrl: pc.exchangeBaseUrl,
- dirty: false,
- transactionPending: false,
- };
- return coin;
- }
-
-
- /**
- * Withdraw coins from a reserve until it is empty.
- */
- private async depleteReserve(reserve: ReserveRecord,
- exchange: IExchangeInfo): Promise<number> {
- if (!reserve.current_amount) {
- throw Error("can't withdraw when amount is unknown");
- }
- let denomsAvailable: Denomination[] = Array.from(exchange.active_denoms);
- let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount!,
- denomsAvailable);
-
- let ps = denomsForWithdraw.map(async(denom) => {
- function mutateReserve(r: ReserveRecord): ReserveRecord {
- let currentAmount = r.current_amount;
- if (!currentAmount) {
- throw Error("can't withdraw when amount is unknown");
- }
- r.precoin_amount = Amounts.add(r.precoin_amount,
- denom.value,
- denom.fee_withdraw).amount;
- let result = Amounts.sub(currentAmount,
- denom.value,
- denom.fee_withdraw);
- if (result.saturated) {
- console.error("can't create precoin, saturated");
- throw AbortTransaction;
- }
- r.current_amount = result.amount;
-
- console.log(`after creating precoin: current ${amountToPretty(r.current_amount)}, precoin: ${amountToPretty(
- r.precoin_amount)})}`);
-
- return r;
- }
-
- let preCoin = await this.cryptoApi
- .createPreCoin(denom, reserve);
- await this.q()
- .put(Stores.precoins, preCoin)
- .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve);
- await this.processPreCoin(preCoin);
- });
-
- await Promise.all(ps);
- return ps.length;
- }
-
-
- /**
- * Update the information about a reserve that is stored in the wallet
- * by quering the reserve's exchange.
- */
- private async updateReserve(reservePub: string,
- exchange: IExchangeInfo): Promise<ReserveRecord> {
- let reserve = await this.q()
- .get<ReserveRecord>(Stores.reserves, reservePub);
- if (!reserve) {
- throw Error("reserve not in db");
- }
- let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl);
- reqUrl.query({'reserve_pub': reservePub});
- let resp = await this.http.get(reqUrl);
- if (resp.status != 200) {
- throw Error();
- }
- let reserveInfo = JSON.parse(resp.responseText);
- if (!reserveInfo) {
- throw Error();
- }
- let oldAmount = reserve.current_amount;
- let newAmount = reserveInfo.balance;
- reserve.current_amount = reserveInfo.balance;
- let historyEntry = {
- type: "reserve-update",
- timestamp: (new Date).getTime(),
- subjectId: `reserve-progress-${reserve.reserve_pub}`,
- detail: {
- reservePub,
- requestedAmount: reserve.requested_amount,
- oldAmount,
- newAmount
- }
- };
- await this.q()
- .put(Stores.reserves, reserve)
- .finish();
- this.notifier.notify();
- return reserve;
- }
-
-
- /**
- * Get the wire information for the exchange with the given base URL.
- */
- async getWireInfo(exchangeBaseUrl: string): Promise<WireInfo> {
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
- let reqUrl = URI("wire").absoluteTo(exchangeBaseUrl);
- let resp = await this.http.get(reqUrl);
-
- if (resp.status != 200) {
- throw Error("/wire request failed");
- }
-
- let wiJson = JSON.parse(resp.responseText);
- if (!wiJson) {
- throw Error("/wire response malformed")
- }
- return wiJson;
- }
-
- async getReserveCreationInfo(baseUrl: string,
- amount: AmountJson): Promise<ReserveCreationInfo> {
- let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
-
- let selectedDenoms = getWithdrawDenomList(amount,
- exchangeInfo.active_denoms);
- let acc = Amounts.getZero(amount.currency);
- for (let d of selectedDenoms) {
- acc = Amounts.add(acc, d.fee_withdraw).amount;
- }
- let actualCoinCost = selectedDenoms
- .map((d: Denomination) => Amounts.add(d.value,
- d.fee_withdraw).amount)
- .reduce((a, b) => Amounts.add(a, b).amount);
-
- let wireInfo = await this.getWireInfo(baseUrl);
-
- let ret: ReserveCreationInfo = {
- exchangeInfo,
- selectedDenoms,
- wireInfo,
- withdrawFee: acc,
- overhead: Amounts.sub(amount, actualCoinCost).amount,
- };
- return ret;
- }
-
-
- /**
- * Update or add exchange DB entry by fetching the /keys information.
- * Optionally link the reserve entry to the new or existing
- * exchange entry in then DB.
- */
- async updateExchangeFromUrl(baseUrl: string): Promise<IExchangeInfo> {
- baseUrl = canonicalizeBaseUrl(baseUrl);
- let reqUrl = URI("keys").absoluteTo(baseUrl);
- let resp = await this.http.get(reqUrl);
- if (resp.status != 200) {
- throw Error("/keys request failed");
- }
- let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
- return this.updateExchangeFromJson(baseUrl, exchangeKeysJson);
- }
-
- private async suspendCoins(exchangeInfo: IExchangeInfo): Promise<void> {
- let suspendedCoins = await (
- this.q()
- .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl)
- .reduce((coin: Coin, suspendedCoins: Coin[]) => {
- if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) {
- return Array.prototype.concat(suspendedCoins, [coin]);
- }
- return Array.prototype.concat(suspendedCoins);
- }, []));
-
- let q = this.q();
- suspendedCoins.map((c) => {
- console.log("suspending coin", c);
- c.suspended = true;
- q.put(Stores.coins, c);
- });
- await q.finish();
- }
-
-
- private async updateExchangeFromJson(baseUrl: string,
- exchangeKeysJson: KeysJson): Promise<IExchangeInfo> {
- const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
- if (updateTimeSec === null) {
- throw Error("invalid update time");
- }
-
- let r = await this.q().get<IExchangeInfo>(Stores.exchanges, baseUrl);
-
- let exchangeInfo: IExchangeInfo;
-
- if (!r) {
- exchangeInfo = {
- baseUrl,
- all_denoms: [],
- active_denoms: [],
- last_update_time: updateTimeSec,
- masterPublicKey: exchangeKeysJson.master_public_key,
- };
- console.log("making fresh exchange");
- } else {
- if (updateTimeSec < r.last_update_time) {
- console.log("outdated /keys, not updating");
- return r
- }
- exchangeInfo = r;
- console.log("updating old exchange");
- }
-
- let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo,
- exchangeKeysJson);
- await this.suspendCoins(updatedExchangeInfo);
-
- await this.q()
- .put(Stores.exchanges, updatedExchangeInfo)
- .finish();
-
- return updatedExchangeInfo;
- }
-
-
- private async updateExchangeInfo(exchangeInfo: IExchangeInfo,
- newKeys: KeysJson): Promise<IExchangeInfo> {
- if (exchangeInfo.masterPublicKey != newKeys.master_public_key) {
- throw Error("public keys do not match");
- }
-
- exchangeInfo.active_denoms = [];
-
- let denomsToCheck = newKeys.denoms.filter((newDenom) => {
- // did we find the new denom in the list of all (old) denoms?
- let found = false;
- for (let oldDenom of exchangeInfo.all_denoms) {
- if (oldDenom.denom_pub === newDenom.denom_pub) {
- let a: any = Object.assign({}, oldDenom);
- let b: any = Object.assign({}, newDenom);
- // pub hash is only there for convenience in the wallet
- delete a["pub_hash"];
- delete b["pub_hash"];
- if (!deepEquals(a, b)) {
- console.error("denomination parameters were modified, old/new:");
- console.dir(a);
- console.dir(b);
- // FIXME: report to auditors
- }
- found = true;
- break;
- }
- }
-
- if (found) {
- exchangeInfo.active_denoms.push(newDenom);
- // No need to check signatures
- return false;
- }
- return true;
- });
-
- let ps = denomsToCheck.map(async(denom) => {
- let valid = await this.cryptoApi
- .isValidDenom(denom,
- exchangeInfo.masterPublicKey);
- if (!valid) {
- console.error("invalid denomination",
- denom,
- "with key",
- exchangeInfo.masterPublicKey);
- // FIXME: report to auditors
- }
- exchangeInfo.active_denoms.push(denom);
- exchangeInfo.all_denoms.push(denom);
- });
-
- await Promise.all(ps);
-
- return exchangeInfo;
- }
-
-
- /**
- * Retrieve a mapping from currency name to the amount
- * that is currenctly available for spending in the wallet.
- */
- async getBalances(): Promise<WalletBalance> {
- function ensureEntry(balance: WalletBalance, currency: string) {
- let entry: WalletBalanceEntry|undefined = balance[currency];
- let z = Amounts.getZero(currency);
- if (!entry) {
- balance[currency] = entry = {
- available: z,
- pendingIncoming: z,
- pendingPayment: z,
- };
- }
- return entry;
- }
-
- function collectBalances(c: Coin, balance: WalletBalance) {
- if (c.suspended) {
- return balance;
- }
- let currency = c.currentAmount.currency;
- let entry = ensureEntry(balance, currency);
- entry.available = Amounts.add(entry.available, c.currentAmount).amount;
- return balance;
- }
-
- function collectPendingWithdraw(r: ReserveRecord, balance: WalletBalance) {
- if (!r.confirmed) {
- return balance;
- }
- let entry = ensureEntry(balance, r.requested_amount.currency);
- let amount = r.current_amount;
- if (!amount) {
- amount = r.requested_amount;
- }
- amount = Amounts.add(amount, r.precoin_amount).amount;
- if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], amount) < 0) {
- entry.pendingIncoming = Amounts.add(entry.pendingIncoming,
- amount).amount;
- }
- return balance;
- }
-
- function collectPendingRefresh(r: RefreshSession, balance: WalletBalance) {
- if (!r.finished) {
- return balance;
- }
- let entry = ensureEntry(balance, r.valueWithFee.currency);
- entry.pendingIncoming = Amounts.add(entry.pendingIncoming,
- r.valueOutput).amount;
-
- return balance;
- }
-
- function collectPayments(t: Transaction, balance: WalletBalance) {
- if (t.finished) {
- return balance;
- }
- let entry = ensureEntry(balance, t.contract.amount.currency);
- entry.pendingPayment = Amounts.add(entry.pendingPayment,
- t.contract.amount).amount;
-
- return balance;
- }
-
- function collectSmallestWithdraw(e: IExchangeInfo, sw: any) {
- let min: AmountJson|undefined;
- for (let d of e.active_denoms) {
- let v = Amounts.add(d.value, d.fee_withdraw).amount;
- if (!min) {
- min = v;
- continue;
- }
- if (Amounts.cmp(v, min) < 0) {
- min = v;
- }
- }
- sw[e.baseUrl] = min;
- return sw;
- }
-
- let balance = {};
- // Mapping from exchange pub to smallest
- // possible amount we can withdraw
- let smallestWithdraw: {[baseUrl: string]: AmountJson} = {};
-
- smallestWithdraw = await (this.q()
- .iter(Stores.exchanges)
- .reduce(collectSmallestWithdraw, {}));
-
- console.log("smallest withdraws", smallestWithdraw);
-
- let tx = this.q();
- tx.iter(Stores.coins)
- .reduce(collectBalances, balance);
- tx.iter(Stores.refresh)
- .reduce(collectPendingRefresh, balance);
- tx.iter(Stores.reserves)
- .reduce(collectPendingWithdraw, balance);
- tx.iter(Stores.transactions)
- .reduce(collectPayments, balance);
- await tx.finish();
- return balance;
-
- }
-
-
- async createRefreshSession(oldCoinPub: string): Promise<RefreshSession|undefined> {
- let coin = await this.q().get<Coin>(Stores.coins, oldCoinPub);
-
- if (!coin) {
- throw Error("coin not found");
- }
-
- let exchange = await this.updateExchangeFromUrl(coin.exchangeBaseUrl);
-
- if (!exchange) {
- throw Error("db inconsistent");
- }
-
- let oldDenom = exchange.all_denoms.find((d) => d.denom_pub == coin!.denomPub);
-
- if (!oldDenom) {
- throw Error("db inconsistent");
- }
-
- let availableDenoms: Denomination[] = exchange.active_denoms;
-
- let availableAmount = Amounts.sub(coin.currentAmount,
- oldDenom.fee_refresh).amount;
-
- let newCoinDenoms = getWithdrawDenomList(availableAmount,
- availableDenoms);
-
- console.log("refreshing into", newCoinDenoms);
-
- if (newCoinDenoms.length == 0) {
- console.log("not refreshing, value too small");
- return undefined;
- }
-
-
- let refreshSession: RefreshSession = await (
- this.cryptoApi.createRefreshSession(exchange.baseUrl,
- 3,
- coin,
- newCoinDenoms,
- oldDenom.fee_refresh));
-
- function mutateCoin(c: Coin): Coin {
- let r = Amounts.sub(c.currentAmount,
- refreshSession.valueWithFee);
- if (r.saturated) {
- // Something else must have written the coin value
- throw AbortTransaction;
- }
- c.currentAmount = r.amount;
- return c;
- }
-
- await this.q()
- .put(Stores.refresh, refreshSession)
- .mutate(Stores.coins, coin.coinPub, mutateCoin)
- .finish();
-
- return refreshSession;
- }
-
-
- async refresh(oldCoinPub: string): Promise<void> {
- let refreshSession: RefreshSession|undefined;
- let oldSession = await this.q().get(Stores.refresh, oldCoinPub);
- if (oldSession) {
- refreshSession = oldSession;
- } else {
- refreshSession = await this.createRefreshSession(oldCoinPub);
- }
- if (!refreshSession) {
- // refreshing not necessary
- return;
- }
- this.continueRefreshSession(refreshSession);
- }
-
- async continueRefreshSession(refreshSession: RefreshSession) {
- if (refreshSession.finished) {
- return;
- }
- if (typeof refreshSession.norevealIndex !== "number") {
- let coinPub = refreshSession.meltCoinPub;
- await this.refreshMelt(refreshSession);
- let r = await this.q().get<RefreshSession>(Stores.refresh, coinPub);
- if (!r) {
- throw Error("refresh session does not exist anymore");
- }
- refreshSession = r;
- }
-
- await this.refreshReveal(refreshSession);
- }
-
-
- async refreshMelt(refreshSession: RefreshSession): Promise<void> {
- if (refreshSession.norevealIndex != undefined) {
- console.error("won't melt again");
- return;
- }
-
- let coin = await this.q().get<Coin>(Stores.coins,
- refreshSession.meltCoinPub);
- if (!coin) {
- console.error("can't melt coin, it does not exist");
- return;
- }
-
- let reqUrl = URI("refresh/melt").absoluteTo(refreshSession.exchangeBaseUrl);
- let meltCoin = {
- coin_pub: coin.coinPub,
- denom_pub: coin.denomPub,
- denom_sig: coin.denomSig,
- confirm_sig: refreshSession.confirmSig,
- value_with_fee: refreshSession.valueWithFee,
- };
- let coinEvs = refreshSession.preCoinsForGammas.map((x) => x.map((y) => y.coinEv));
- let req = {
- "new_denoms": refreshSession.newDenoms,
- "melt_coin": meltCoin,
- "transfer_pubs": refreshSession.transferPubs,
- "coin_evs": coinEvs,
- };
- console.log("melt request:", req);
- let resp = await this.http.postJson(reqUrl, req);
-
- console.log("melt request:", req);
- console.log("melt response:", resp.responseText);
-
- if (resp.status != 200) {
- console.error(resp.responseText);
- throw Error("refresh failed");
- }
-
- let respJson = JSON.parse(resp.responseText);
-
- if (!respJson) {
- throw Error("exchange responded with garbage");
- }
-
- let norevealIndex = respJson.noreveal_index;
-
- if (typeof norevealIndex != "number") {
- throw Error("invalid response");
- }
-
- refreshSession.norevealIndex = norevealIndex;
-
- await this.q().put(Stores.refresh, refreshSession).finish();
- }
-
-
- async refreshReveal(refreshSession: RefreshSession): Promise<void> {
- let norevealIndex = refreshSession.norevealIndex;
- if (norevealIndex == undefined) {
- throw Error("can't reveal without melting first");
- }
- let privs = Array.from(refreshSession.transferPrivs);
- privs.splice(norevealIndex, 1);
-
- let req = {
- "session_hash": refreshSession.hash,
- "transfer_privs": privs,
- };
-
- let reqUrl = URI("refresh/reveal")
- .absoluteTo(refreshSession.exchangeBaseUrl);
- console.log("reveal request:", req);
- let resp = await this.http.postJson(reqUrl, req);
-
- console.log("session:", refreshSession);
- console.log("reveal response:", resp);
-
- if (resp.status != 200) {
- console.log("error: /refresh/reveal returned status " + resp.status);
- return;
- }
-
- let respJson = JSON.parse(resp.responseText);
-
- if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
- console.log("/refresh/reveal did not contain ev_sigs");
- }
-
- let exchange = await this.q().get<IExchangeInfo>(Stores.exchanges,
- refreshSession.exchangeBaseUrl);
- if (!exchange) {
- console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`);
- return;
- }
-
- let coins: Coin[] = [];
-
- for (let i = 0; i < respJson.ev_sigs.length; i++) {
- let denom = exchange.all_denoms.find((d) => d.denom_pub == refreshSession.newDenoms[i]);
- if (!denom) {
- console.error("denom not found");
- continue;
- }
- let pc = refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i];
- let denomSig = await this.cryptoApi.rsaUnblind(respJson.ev_sigs[i].ev_sig,
- pc.blindingKey,
- denom.denom_pub);
- let coin: Coin = {
- coinPub: pc.publicKey,
- coinPriv: pc.privateKey,
- denomPub: denom.denom_pub,
- denomSig: denomSig,
- currentAmount: denom.value,
- exchangeBaseUrl: refreshSession.exchangeBaseUrl,
- dirty: false,
- transactionPending: false,
- };
-
- coins.push(coin);
- }
-
- refreshSession.finished = true;
-
- await this.q()
- .putAll(Stores.coins, coins)
- .put(Stores.refresh, refreshSession)
- .finish();
- }
-
-
- /**
- * Retrive the full event history for this wallet.
- */
- async getHistory(): Promise<any> {
- function collect(x: any, acc: any) {
- acc.push(x);
- return acc;
- }
-
- let history = await (
- this.q()
- .iterIndex(Stores.history.timestampIndex)
- .reduce(collect, []));
-
- return {history};
- }
-
-
- async getOffer(offerId: number): Promise<any> {
- let offer = await this.q() .get(Stores.offers, offerId);
- return offer;
- }
-
- async getExchanges(): Promise<IExchangeInfo[]> {
- return this.q()
- .iter<IExchangeInfo>(Stores.exchanges)
- .flatMap((e) => [e])
- .toArray();
- }
-
- async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
- return this.q()
- .iter<ReserveRecord>(Stores.reserves)
- .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl)
- .toArray();
- }
-
- async getCoins(exchangeBaseUrl: string): Promise<Coin[]> {
- return this.q()
- .iter<Coin>(Stores.coins)
- .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl)
- .toArray();
- }
-
- async getPreCoins(exchangeBaseUrl: string): Promise<PreCoin[]> {
- return this.q()
- .iter<PreCoin>(Stores.precoins)
- .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl)
- .toArray();
- }
-
- async hashContract(contract: Contract): Promise<string> {
- return this.cryptoApi.hashString(canonicalJson(contract));
- }
-
- /**
- * Check if there's an equivalent contract we've already purchased.
- */
- async checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> {
- if (!contract.repurchase_correlation_id) {
- console.log("no repurchase: no correlation id");
- return {isRepurchase: false};
- }
- let result: Transaction|undefined = await (
- this.q()
- .getIndexed(Stores.transactions.repurchaseIndex,
- [
- contract.merchant_pub,
- contract.repurchase_correlation_id
- ]));
-
- if (result) {
- console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id);
- return {
- isRepurchase: true,
- existingContractHash: result.contractHash,
- existingFulfillmentUrl: result.contract.fulfillment_url,
- };
- } else {
- return {isRepurchase: false};
- }
- }
-
-
- async paymentSucceeded(contractHash: string): Promise<any> {
- const doPaymentSucceeded = async() => {
- let t = await this.q().get<Transaction>(Stores.transactions,
- contractHash);
- if (!t) {
- console.error("contract not found");
- return;
- }
- t.finished = true;
- let modifiedCoins: Coin[] = [];
- for (let pc of t.payReq.coins) {
- let c = await this.q().get<Coin>(Stores.coins, pc.coin_pub);
- if (!c) {
- console.error("coin not found");
- return;
- }
- c.transactionPending = false;
- modifiedCoins.push(c);
- }
-
- await this.q()
- .putAll(Stores.coins, modifiedCoins)
- .put(Stores.transactions, t)
- .finish();
- for (let c of t.payReq.coins) {
- this.refresh(c.coin_pub);
- }
- };
- doPaymentSucceeded();
- return;
- }
-}