aboutsummaryrefslogtreecommitdiff
path: root/lib/wallet/wallet.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/wallet/wallet.ts')
-rw-r--r--lib/wallet/wallet.ts957
1 files changed, 957 insertions, 0 deletions
diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts
new file mode 100644
index 000000000..92fb92a4a
--- /dev/null
+++ b/lib/wallet/wallet.ts
@@ -0,0 +1,957 @@
+/*
+ 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, 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, IMintInfo, Denomination, Notifier} 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";
+
+"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;
+}
+
+
+class MintInfo implements IMintInfo {
+ baseUrl: string;
+ masterPublicKey: string;
+ denoms: Denomination[];
+
+ constructor(obj: {baseUrl: string} & any) {
+ this.baseUrl = obj.baseUrl;
+
+ if (obj.denoms) {
+ this.denoms = Array.from(<Denomination[]>obj.denoms);
+ } else {
+ this.denoms = [];
+ }
+
+ if (typeof obj.masterPublicKey === "string") {
+ this.masterPublicKey = obj.masterPublicKey;
+ }
+ }
+
+ static fresh(baseUrl: string): MintInfo {
+ return new MintInfo({baseUrl});
+ }
+
+ /**
+ * Merge new key information into the mint info.
+ * If the new key information is invalid (missing fields,
+ * invalid signatures), an exception is thrown, but the
+ * mint info is updated with the new information up until
+ * the first error.
+ */
+ mergeKeys(newKeys: KeysJson, cryptoApi: CryptoApi): Promise<void> {
+ if (!this.masterPublicKey) {
+ this.masterPublicKey = newKeys.master_public_key;
+ }
+
+ if (this.masterPublicKey != newKeys.master_public_key) {
+ throw Error("public keys do not match");
+ }
+
+ let ps = newKeys.denoms.map((newDenom) => {
+ let found = false;
+ for (let oldDenom of this.denoms) {
+ if (oldDenom.denom_pub === newDenom.denom_pub) {
+ let a = Object.assign({}, oldDenom);
+ let b = 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.log("old/new:");
+ console.dir(a);
+ console.dir(b);
+ throw Error("denomination modified");
+ }
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ return Promise.resolve();
+ }
+
+ return cryptoApi
+ .isValidDenom(newDenom, this.masterPublicKey)
+ .then((valid) => {
+ if (!valid) {
+ throw Error("signature on denomination invalid");
+ }
+ return cryptoApi.hashRsaPub(newDenom.denom_pub);
+ })
+ .then((h) => {
+ this.denoms.push(Object.assign({}, newDenom, {pub_hash: h}));
+ });
+ });
+
+ return Promise.all(ps).then(() => void 0);
+ }
+}
+
+
+@Checkable.Class
+export class CreateReserveRequest {
+ /**
+ * The initial amount for the reserve.
+ */
+ @Checkable.Value(AmountJson)
+ amount: AmountJson;
+
+ /**
+ * Mint URL where the bank should create the reserve.
+ */
+ @Checkable.String
+ mint: 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 MintHandle {
+ @Checkable.String
+ master_pub: string;
+
+ @Checkable.String
+ url: string;
+
+ static checked: (obj: any) => MintHandle;
+}
+
+
+@Checkable.Class
+export class Contract {
+ @Checkable.String
+ H_wire: string;
+
+ @Checkable.Value(AmountJson)
+ amount: AmountJson;
+
+ @Checkable.List(Checkable.AnyObject)
+ auditors: any[];
+
+ @Checkable.String
+ expiry: string;
+
+ @Checkable.Any
+ locations: any;
+
+ @Checkable.Value(AmountJson)
+ max_fee: AmountJson;
+
+ @Checkable.Any
+ merchant: any;
+
+ @Checkable.String
+ merchant_pub: string;
+
+ @Checkable.List(Checkable.Value(MintHandle))
+ mints: MintHandle[];
+
+ @Checkable.List(Checkable.AnyObject)
+ products: any[];
+
+ @Checkable.String
+ refund_deadline: string;
+
+ @Checkable.String
+ timestamp: string;
+
+ @Checkable.Number
+ transaction_id: number;
+
+ @Checkable.String
+ fulfillment_url: string;
+
+ @Checkable.Optional(Checkable.String)
+ repurchase_correlation_id: string;
+
+ static checked: (obj: any) => Contract;
+}
+
+
+@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;
+}
+
+
+interface ConfirmPayRequest {
+ offer: Offer;
+}
+
+interface MintCoins {
+ [mintUrl: string]: CoinWithDenom[];
+}
+
+
+interface CoinPaySig {
+ coin_sig: string;
+ coin_pub: string;
+ ub_sig: string;
+ denom_pub: string;
+ f: AmountJson;
+}
+
+
+interface Transaction {
+ contractHash: string;
+ contract: Contract;
+ payReq: any;
+ merchantSig: string;
+}
+
+
+export interface Badge {
+ setText(s: string): void;
+ setColor(c: string): void;
+}
+
+
+function deepEquals(x, y) {
+ 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 getTalerStampSec(stamp: string) {
+ const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
+ if (!m) {
+ return null;
+ }
+ return parseInt(m[1]);
+}
+
+
+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): Promise<HttpResponse>;
+
+ postForm(url: string|uri.URI, form): Promise<HttpResponse>;
+}
+
+
+function copy(o) {
+ return JSON.parse(JSON.stringify(o));
+}
+
+
+/**
+ * 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);
+ let 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;
+
+
+ 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();
+ }
+
+
+ /**
+ * Get mints and associated coins that are still spendable,
+ * but only if the sum the coins' remaining value exceeds the payment amount.
+ */
+ private getPossibleMintCoins(paymentAmount: AmountJson,
+ depositFeeLimit: AmountJson,
+ allowedMints: MintHandle[]): Promise<MintCoins> {
+ // Mapping from mint base URL to list of coins together with their
+ // denomination
+ let m: MintCoins = {};
+
+ function storeMintCoin(mc) {
+ let mint: IMintInfo = mc[0];
+ let coin: Coin = mc[1];
+ let cd = {
+ coin: coin,
+ denom: mint.denoms.find((e) => e.denom_pub === coin.denomPub)
+ };
+ if (!cd.denom) {
+ throw Error("denom not found (database inconsistent)");
+ }
+ if (cd.denom.value.currency !== paymentAmount.currency) {
+ console.warn("same pubkey for different currencies");
+ return;
+ }
+ let x = m[mint.baseUrl];
+ if (!x) {
+ m[mint.baseUrl] = [cd];
+ } else {
+ x.push(cd);
+ }
+ }
+
+ let ps = allowedMints.map((info) => {
+ console.log("Checking for merchant's mint", JSON.stringify(info));
+ return Query(this.db)
+ .iter("mints", {indexName: "pubKey", only: info.master_pub})
+ .indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl)
+ .reduce(storeMintCoin);
+ });
+
+ return Promise.all(ps).then(() => {
+ let ret: MintCoins = {};
+
+ if (Object.keys(m).length == 0) {
+ console.log("not suitable mints found");
+ }
+
+ console.dir(m);
+
+ // We try to find the first mint where we have
+ // enough coins to cover the paymentAmount with fees
+ // under depositFeeLimit
+
+ nextMint:
+ 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 nextMint;
+ }
+ usableCoins.push(coins[i]);
+ if (Amounts.cmp(accAmount, minAmount) >= 0) {
+ ret[key] = usableCoins;
+ continue nextMint;
+ }
+ }
+ }
+ return ret;
+ });
+ }
+
+
+ /**
+ * Record all information that is necessary to
+ * pay for a contract in the wallet's database.
+ */
+ private recordConfirmPay(offer: Offer,
+ payCoinInfo: PayCoinInfo,
+ chosenMint: string): Promise<void> {
+ let payReq = {};
+ 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["mint"] = URI(chosenMint).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,
+ };
+
+ console.log("pay request");
+ console.dir(payReq);
+
+ let historyEntry = {
+ type: "pay",
+ timestamp: (new Date).getTime(),
+ detail: {
+ merchantName: offer.contract.merchant.name,
+ amount: offer.contract.amount,
+ contractHash: offer.H_contract,
+ fulfillmentUrl: offer.contract.fulfillment_url
+ }
+ };
+
+ return Query(this.db)
+ .put("transactions", t)
+ .put("history", historyEntry)
+ .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin))
+ .finish()
+ .then(() => {
+ this.notifier.notify();
+ });
+ }
+
+
+ /**
+ * Add a contract to the wallet and sign coins,
+ * but do not send them yet.
+ */
+ confirmPay(offer: Offer): Promise<any> {
+ console.log("executing confirmPay");
+ return Promise.resolve().then(() => {
+ return this.getPossibleMintCoins(offer.contract.amount,
+ offer.contract.max_fee,
+ offer.contract.mints)
+ }).then((mcs) => {
+ if (Object.keys(mcs).length == 0) {
+ console.log("not confirming payment, insufficient coins");
+ return {
+ error: "coins-insufficient",
+ };
+ }
+ let mintUrl = Object.keys(mcs)[0];
+
+ return this.cryptoApi.signDeposit(offer, mcs[mintUrl])
+ .then((ds) => this.recordConfirmPay(offer, ds, mintUrl))
+ .then(() => ({}));
+ });
+ }
+
+
+ /**
+ * Retrieve all necessary information for looking up the contract
+ * with the given hash.
+ */
+ executePayment(H_contract): Promise<any> {
+ return Promise.resolve().then(() => {
+ return Query(this.db)
+ .get("transactions", H_contract)
+ .then((t) => {
+ 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 initReserve(reserveRecord) {
+ this.updateMintFromUrl(reserveRecord.mint_base_url)
+ .then((mint) =>
+ this.updateReserve(reserveRecord.reserve_pub, mint)
+ .then((reserve) => this.depleteReserve(reserve,
+ mint)))
+ .then(() => {
+ let depleted = {
+ type: "depleted-reserve",
+ timestamp: (new Date).getTime(),
+ detail: {
+ reservePub: reserveRecord.reserve_pub,
+ }
+ };
+ return Query(this.db).put("history", depleted).finish();
+ })
+ .catch((e) => {
+ console.error("Failed to deplete reserve");
+ console.error(e);
+ });
+ }
+
+
+ /**
+ * Create a reserve, but do not flag it as confirmed yet.
+ */
+ createReserve(req: CreateReserveRequest): Promise<CreateReserveResponse> {
+ return this.cryptoApi.createEddsaKeypair().then((keypair) => {
+ const now = (new Date).getTime();
+ const canonMint = canonicalizeBaseUrl(req.mint);
+
+ const reserveRecord = {
+ reserve_pub: keypair.pub,
+ reserve_priv: keypair.priv,
+ mint_base_url: canonMint,
+ created: now,
+ last_query: null,
+ current_amount: null,
+ requested_amount: req.amount,
+ confirmed: false,
+ };
+
+
+ const historyEntry = {
+ type: "create-reserve",
+ timestamp: now,
+ detail: {
+ requestedAmount: req.amount,
+ reservePub: reserveRecord.reserve_pub,
+ }
+ };
+
+ return Query(this.db)
+ .put("reserves", reserveRecord)
+ .put("history", historyEntry)
+ .finish()
+ .then(() => {
+ let r: CreateReserveResponse = {
+ mint: canonMint,
+ 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 mint 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.
+ */
+ confirmReserve(req: ConfirmReserveRequest): Promise<void> {
+ const now = (new Date).getTime();
+ const historyEntry = {
+ type: "confirm-reserve",
+ timestamp: now,
+ detail: {
+ reservePub: req.reservePub,
+ }
+ };
+ return Query(this.db)
+ .get("reserves", req.reservePub)
+ .then((r) => {
+ r.confirmed = true;
+ return Query(this.db)
+ .put("reserves", r)
+ .put("history", historyEntry)
+ .finish()
+ .then(() => {
+ // Do this in the background
+ this.initReserve(r);
+ });
+ });
+ }
+
+
+ private withdrawExecute(pc: PreCoin): Promise<Coin> {
+ return Query(this.db)
+ .get("reserves", pc.reservePub)
+ .then((r) => {
+ 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(r.mint_base_url);
+ return this.http.postJson(reqUrl, wd);
+ })
+ .then(resp => {
+ if (resp.status != 200) {
+ throw new RequestException({
+ hint: "Withdrawal failed",
+ status: resp.status
+ });
+ }
+ let r = JSON.parse(resp.responseText);
+ return this.cryptoApi.rsaUnblind(r.ev_sig, pc.blindingKey, pc.denomPub)
+ .then((denomSig) => {
+ let coin: Coin = {
+ coinPub: pc.coinPub,
+ coinPriv: pc.coinPriv,
+ denomPub: pc.denomPub,
+ denomSig: denomSig,
+ currentAmount: pc.coinValue,
+ mintBaseUrl: pc.mintBaseUrl,
+ };
+ return coin;
+
+ });
+ });
+ }
+
+ storeCoin(coin: Coin): Promise<void> {
+ let historyEntry = {
+ type: "withdraw",
+ timestamp: (new Date).getTime(),
+ detail: {
+ coinPub: coin.coinPub,
+ }
+ };
+ return Query(this.db)
+ .delete("precoins", coin.coinPub)
+ .add("coins", coin)
+ .add("history", historyEntry)
+ .finish()
+ .then(() => {
+ this.notifier.notify();
+ });
+ }
+
+
+ /**
+ * Withdraw one coins of the given denomination from the given reserve.
+ */
+ private withdraw(denom: Denomination, reserve: Reserve): Promise<void> {
+ console.log("creating pre coin at", new Date());
+ return this.cryptoApi
+ .createPreCoin(denom, reserve)
+ .then((preCoin) => {
+ return Query(this.db)
+ .put("precoins", preCoin)
+ .finish()
+ .then(() => this.withdrawExecute(preCoin))
+ .then((c) => this.storeCoin(c));
+ });
+
+ }
+
+
+ /**
+ * Withdraw coins from a reserve until it is empty.
+ */
+ private depleteReserve(reserve, mint: MintInfo): Promise<void> {
+ let denomsAvailable: Denomination[] = copy(mint.denoms);
+ let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
+ denomsAvailable);
+
+ let ps = denomsForWithdraw.map((denom) => {
+ console.log("withdrawing", JSON.stringify(denom));
+ // Do the withdraw asynchronously, so crypto is interleaved
+ // with requests
+ return this.withdraw(denom, reserve);
+ });
+
+ return Promise.all(ps).then(() => void 0);
+ }
+
+
+ /**
+ * Update the information about a reserve that is stored in the wallet
+ * by quering the reserve's mint.
+ */
+ private updateReserve(reservePub: string, mint: MintInfo): Promise<Reserve> {
+ return Query(this.db)
+ .get("reserves", reservePub)
+ .then((reserve) => {
+ let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl);
+ reqUrl.query({'reserve_pub': reservePub});
+ return this.http.get(reqUrl).then(resp => {
+ 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(),
+ detail: {
+ reservePub,
+ oldAmount,
+ newAmount
+ }
+ };
+ return Query(this.db)
+ .put("reserves", reserve)
+ .finish()
+ .then(() => reserve);
+ });
+ });
+ }
+
+
+ getReserveCreationInfo(baseUrl: string,
+ amount: AmountJson): Promise<ReserveCreationInfo> {
+ return this.updateMintFromUrl(baseUrl)
+ .then((mintInfo: IMintInfo) => {
+ let selectedDenoms = getWithdrawDenomList(amount,
+ mintInfo.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 ret: ReserveCreationInfo = {
+ mintInfo,
+ selectedDenoms,
+ withdrawFee: acc,
+ overhead: Amounts.sub(amount, actualCoinCost).amount,
+ };
+ return ret;
+ });
+ }
+
+
+ /**
+ * Update or add mint DB entry by fetching the /keys information.
+ * Optionally link the reserve entry to the new or existing
+ * mint entry in then DB.
+ */
+ updateMintFromUrl(baseUrl): Promise<MintInfo> {
+ baseUrl = canonicalizeBaseUrl(baseUrl);
+ let reqUrl = URI("keys").absoluteTo(baseUrl);
+ return this.http.get(reqUrl).then((resp) => {
+ if (resp.status != 200) {
+ throw Error("/keys request failed");
+ }
+ let mintKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
+
+ return Query(this.db).get("mints", baseUrl).then((r) => {
+ let mintInfo;
+ console.dir(r);
+
+ if (!r) {
+ mintInfo = MintInfo.fresh(baseUrl);
+ console.log("making fresh mint");
+ } else {
+ mintInfo = new MintInfo(r);
+ console.log("using old mint");
+ }
+
+ return mintInfo.mergeKeys(mintKeysJson, this.cryptoApi)
+ .then(() => {
+ return Query(this.db)
+ .put("mints", mintInfo)
+ .finish()
+ .then(() => mintInfo);
+ });
+
+ });
+ });
+ }
+
+
+ /**
+ * Retrieve a mapping from currency name to the amount
+ * that is currenctly available for spending in the wallet.
+ */
+ getBalances(): Promise<any> {
+ function collectBalances(c: Coin, 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;
+ }
+
+ return Query(this.db)
+ .iter("coins")
+ .reduce(collectBalances, {});
+ }
+
+
+ /**
+ * Retrive the full event history for this wallet.
+ */
+ getHistory(): Promise<any[]> {
+ function collect(x, acc) {
+ acc.push(x);
+ return acc;
+ }
+
+ return Query(this.db)
+ .iter("history", {indexName: "timestamp"})
+ .reduce(collect, [])
+ }
+
+ checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> {
+ if (!contract.repurchase_correlation_id) {
+ console.log("no repurchase: no correlation id");
+ return Promise.resolve({isRepurchase: false});
+ }
+ return Query(this.db)
+ .getIndexed("transactions",
+ "repurchase",
+ [contract.merchant_pub, contract.repurchase_correlation_id])
+ .then((result: Transaction) => {
+ console.log("db result", result);
+ let isRepurchase;
+ 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};
+ }
+ });
+ }
+} \ No newline at end of file