/*
 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,
  CreateReserveResponse,
  IExchangeInfo,
  Denomination,
  Notifier,
  WireInfo
} from "./types";
import {HttpResponse, RequestException} from "./http";
import {Query} from "./query";
import {Checkable} from "./checkable";
import {canonicalizeBaseUrl} from "./helpers";
import {ReserveCreationInfo, Amounts} from "./types";
import {PreCoin} from "./types";
import {Reserve} from "./types";
import {CryptoApi} from "./cryptoApi";
import {Coin} from "./types";
import {PayCoinInfo} from "./types";
import {CheckRepurchaseResult} from "./types";
import {Contract} from "./types";
import {ExchangeHandle} from "./types";

"use strict";

export interface CoinWithDenom {
  coin: Coin;
  denom: Denomination;
}

interface ReserveRecord {
  reserve_pub: string;
  reserve_priv: string,
  exchange_base_url: string,
  created: number,
  last_query: number|null,
  /**
   * Current amount left in the reserve
   */
  current_amount: AmountJson|null,
  /**
   * Amount requested when the reserve was created.
   * When a reserve is re-used (rare!)  the current_amount can
   * be higher than the requested_amount
   */
  requested_amount: AmountJson,
  /**
   * Amount we've already withdrawn from the reserve.
   */
  withdrawn_amount: AmountJson;
  confirmed: boolean,
}


@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;

  static checked: (obj: any) => Offer;
}

export interface HistoryRecord {
  type: string;
  timestamp: number;
  subjectId?: string;
  detail: any;
  level: HistoryLevel;
}


interface ExchangeCoins {
  [exchangeUrl: string]: CoinWithDenom[];
}


interface Transaction {
  contractHash: string;
  contract: Contract;
  payReq: any;
  merchantSig: string;
}

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;
}

export function canonicalJson(obj: any): string {
  // Check for cycles, etc.
  JSON.stringify(obj);
  if (typeof obj == "string" || typeof obj == "number" || obj === null) {
    return JSON.stringify(obj)
  }
  if (Array.isArray(obj)) {
    let objs: string[] = obj.map((e) => canonicalJson(e));
    return `[${objs.join(',')}]`;
  }
  let keys: string[] = [];
  for (let key in obj) {
    keys.push(key);
  }
  keys.sort();
  let s = "{";
  for (let i = 0; i < keys.length; i++) {
    let key = keys[i];
    s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
    if (i != keys.length - 1) {
      s += ",";
    }
  }
  return s + "}";
}


function deepEquals(x: any, y: any): boolean {
  if (x === y) {
    return true;
  }

  if (Array.isArray(x) && x.length !== y.length) {
    return false;
  }

  var p = Object.keys(x);
  return Object.keys(y).every((i) => p.indexOf(i) !== -1) &&
    p.every((i) => deepEquals(x[i], y[i]));
}


function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
  return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
}


function getTalerStampSec(stamp: string): number|null {
  const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
  if (!m) {
    return null;
  }
  return parseInt(m[1]);
}


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);
  // Withdraw if still possible to withdraw within a minute
  if (stamp_withdraw_sec + 60 > now_sec) {
    return true;
  }
  return false;
}


interface HttpRequestLibrary {
  req(method: string,
      url: string|uri.URI,
      options?: any): Promise<HttpResponse>;

  get(url: string|uri.URI): Promise<HttpResponse>;

  postJson(url: string|uri.URI, body: any): Promise<HttpResponse>;

  postForm(url: string|uri.URI, form: any): Promise<HttpResponse>;
}


function copy(o: any) {
  return JSON.parse(JSON.stringify(o));
}

/**
 * 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 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();

  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();
    }
  }

  updateExchanges(): void {
    console.log("updating exchanges");

    Query(this.db)
      .iter("exchanges")
      .reduce((exchange: IExchangeInfo) => {
        this.updateExchangeFromUrl(exchange.baseUrl)
            .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");

    Query(this.db)
      .iter("reserves")
      .reduce((reserve: any) => {
        console.log("resuming reserve", reserve.reserve_pub);
        this.processReserve(reserve);
      });

    Query(this.db)
      .iter("precoins")
      .reduce((preCoin: any) => {
        console.log("resuming precoin");
        this.processPreCoin(preCoin);
      });
  }


  /**
   * 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: any, url: string) {
      let exchange: IExchangeInfo = mc[0];
      console.log("got coin for exchange", url);
      let coin: Coin = mc[1];
      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 [
        Query(this.db)
          .iter("exchanges", {indexName: "pubKey", only: info.master_pub})
          .indexJoin("coins", "exchangeBaseUrl", (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.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: any = {};
    payReq["amount"] = offer.contract.amount;
    payReq["coins"] = payCoinInfo.map((x) => x.sig);
    payReq["H_contract"] = offer.H_contract;
    payReq["max_fee"] = offer.contract.max_fee;
    payReq["merchant_sig"] = offer.merchant_sig;
    payReq["exchange"] = URI(chosenExchange).href();
    payReq["refund_deadline"] = offer.contract.refund_deadline;
    payReq["timestamp"] = offer.contract.timestamp;
    payReq["transaction_id"] = offer.contract.transaction_id;
    let t: Transaction = {
      contractHash: offer.H_contract,
      contract: offer.contract,
      payReq: payReq,
      merchantSig: offer.merchant_sig,
    };

    let historyEntry = {
      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,
      }
    };

    await Query(this.db)
      .put("transactions", t)
      .put("history", historyEntry)
      .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin))
      .finish();

    this.notifier.notify();
  }


  async putHistory(historyEntry: HistoryRecord): Promise<void> {
    await Query(this.db).put("history", historyEntry).finish();
    this.notifier.notify();
  }


  /**
   * 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 Query(this.db)
      .get("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
      Query(this.db)
        .get("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 Query(this.db)
      .get("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 = {
          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,
          }
        };
        await Query(this.db).put("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> {
    try {
      const coin = await this.withdrawExecute(preCoin);
      this.storeCoin(coin);
    } 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,
      withdrawn_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 Query(this.db)
      .put("reserves", reserveRecord)
      .put("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 = await Query(this.db)
      .get("reserves", req.reservePub);
    const historyEntry = {
      type: "confirm-reserve",
      timestamp: now,
      subjectId: `reserve-progress-${reserve.reserve_pub}`,
      detail: {
        exchangeBaseUrl: reserve.exchange_base_url,
        reservePub: req.reservePub,
        requestedAmount: reserve.requested_amount,
      }
    };
    if (!reserve) {
      console.error("Unable to confirm reserve, not found in DB");
      return;
    }
    reserve.confirmed = true;
    await Query(this.db)
      .put("reserves", reserve)
      .put("history", historyEntry)
      .finish();

    this.processReserve(reserve);
  }


  private async withdrawExecute(pc: PreCoin): Promise<Coin> {
    let reserve = await Query(this.db)
      .get("reserves", pc.reservePub);

    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,
    };
    return coin;
  }

  async storeCoin(coin: Coin): Promise<void> {
    console.log("storing coin", new Date());

    let historyEntry: HistoryRecord = {
      type: "withdraw",
      timestamp: (new Date).getTime(),
      level: HistoryLevel.Expert,
      detail: {
        coinPub: coin.coinPub,
      }
    };
    await Query(this.db)
      .delete("precoins", coin.coinPub)
      .add("coins", coin)
      .add("history", historyEntry)
      .finish();
    this.notifier.notify();
  }


  /**
   * Withdraw one coin of the given denomination from the given reserve.
   */
  private async withdraw(denom: Denomination, reserve: Reserve): Promise<void> {
    console.log("creating pre coin at", new Date());
    let preCoin = await this.cryptoApi
                            .createPreCoin(denom, reserve);
    await Query(this.db)
      .put("precoins", preCoin)
      .finish();
    await this.processPreCoin(preCoin);
  }


  /**
   * Withdraw coins from a reserve until it is empty.
   */
  private async depleteReserve(reserve: any,
                               exchange: IExchangeInfo): Promise<number> {
    let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
    let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
                                                 denomsAvailable);

    let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve));
    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<Reserve> {
    let reserve = await Query(this.db)
      .get("reserves", reservePub);
    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 Query(this.db)
      .put("reserves", reserve)
      .finish();
    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 Query(this.db)
      .iter("coins",
            {indexName: "exchangeBaseUrl", only: 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 = Query(this.db);
    suspendedCoins.map((c) => {
      console.log("suspending coin", c);
      c.suspended = true;
      q.put("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 Query(this.db).get("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 Query(this.db)
      .put("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<any> {
    function collectBalances(c: Coin, byCurrency: any) {
      if (c.suspended) {
        return byCurrency;
      }
      let acc: AmountJson = byCurrency[c.currentAmount.currency];
      if (!acc) {
        acc = Amounts.getZero(c.currentAmount.currency);
      }
      byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount,
                                                         acc).amount;
      return byCurrency;
    }

    let byCurrency = await Query(this.db)
      .iter("coins")
      .reduce(collectBalances, {});

    return {balances: byCurrency};
  }


  /**
   * 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
      Query(this.db)
        .iter("history", {indexName: "timestamp"})
        .reduce(collect, []);

    return {history};
  }

  async hashContract(contract: any): 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 = await Query(this.db)
      .getIndexed("transactions",
                  "repurchase",
                  [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};
    }
  }
}