refactoring / refresh WIP

This commit is contained in:
Florian Dold 2016-10-13 02:23:24 +02:00
parent 122e069d91
commit 250069d860
9 changed files with 453 additions and 274 deletions

View File

@ -21,12 +21,12 @@
*/ */
import {PreCoin} from "./types"; import {PreCoin, Coin, ReserveRecord, AmountJson} from "./types";
import {Reserve} from "./types";
import {Denomination} from "./types"; import {Denomination} from "./types";
import {Offer} from "./wallet"; import {Offer} from "./wallet";
import {CoinWithDenom} from "./wallet"; import {CoinWithDenom} from "./wallet";
import {PayCoinInfo} from "./types"; import {PayCoinInfo} from "./types";
import {RefreshSession} from "./types";
interface RegistryEntry { interface RegistryEntry {
resolve: any; resolve: any;
@ -228,7 +228,7 @@ export class CryptoApi {
} }
createPreCoin(denom: Denomination, reserve: Reserve): Promise<PreCoin> { createPreCoin(denom: Denomination, reserve: ReserveRecord): Promise<PreCoin> {
return this.doRpc("createPreCoin", 1, denom, reserve); return this.doRpc("createPreCoin", 1, denom, reserve);
} }
@ -257,4 +257,17 @@ export class CryptoApi {
rsaUnblind(sig: string, bk: string, pk: string): Promise<string> { rsaUnblind(sig: string, bk: string, pk: string): Promise<string> {
return this.doRpc("rsaUnblind", 4, sig, bk, pk); return this.doRpc("rsaUnblind", 4, sig, bk, pk);
} }
createWithdrawSession(kappa: number, meltCoin: Coin,
newCoinDenoms: Denomination[],
meltAmount: AmountJson,
meltFee: AmountJson): Promise<RefreshSession> {
return this.doRpc("createWithdrawSession",
4,
kappa,
meltCoin,
newCoinDenoms,
meltAmount,
meltFee);
}
} }

View File

@ -22,13 +22,21 @@
"use strict"; "use strict";
import * as native from "./emscriptif"; import * as native from "./emscriptif";
import {PreCoin, Reserve, PayCoinInfo} from "./types"; import {
PreCoin, PayCoinInfo, AmountJson,
RefreshSession, RefreshPreCoin, ReserveRecord
} from "./types";
import create = chrome.alarms.create; import create = chrome.alarms.create;
import {Offer} from "./wallet"; import {Offer} from "./wallet";
import {CoinWithDenom} from "./wallet"; import {CoinWithDenom} from "./wallet";
import {CoinPaySig} from "./types"; import {CoinPaySig} from "./types";
import {Denomination} from "./types"; import {Denomination} from "./types";
import {Amount} from "./emscriptif"; import {Amount} from "./emscriptif";
import {Coin} from "../../background/lib/wallet/types";
import {HashContext} from "./emscriptif";
import {RefreshMeltCoinAffirmationPS} from "./emscriptif";
import {EddsaPublicKey} from "./emscriptif";
import {HashCode} from "./emscriptif";
export function main(worker: Worker) { export function main(worker: Worker) {
@ -61,7 +69,7 @@ namespace RpcFunctions {
* reserve. * reserve.
*/ */
export function createPreCoin(denom: Denomination, export function createPreCoin(denom: Denomination,
reserve: Reserve): PreCoin { reserve: ReserveRecord): PreCoin {
let reservePriv = new native.EddsaPrivateKey(); let reservePriv = new native.EddsaPrivateKey();
reservePriv.loadCrock(reserve.reserve_priv); reservePriv.loadCrock(reserve.reserve_priv);
let reservePub = new native.EddsaPublicKey(); let reservePub = new native.EddsaPublicKey();
@ -224,4 +232,82 @@ namespace RpcFunctions {
} }
return ret; return ret;
} }
}
function createWithdrawSession(kappa: number, meltCoin: Coin,
newCoinDenoms: Denomination[],
meltAmount: AmountJson,
meltFee: AmountJson): RefreshSession {
let sessionHc = new HashContext();
let transferPubs: string[] = [];
let preCoinsForGammas: RefreshPreCoin[][] = [];
for (let i = 0; i < newCoinDenoms.length; i++) {
let t = native.EcdsaPrivateKey.create();
sessionHc.read(t);
transferPubs.push(t.toCrock());
}
for (let i = 0; i < newCoinDenoms.length; i++) {
let r = native.RsaPublicKey.fromCrock(newCoinDenoms[i].denom_pub);
sessionHc.read(r.encode());
}
sessionHc.read(native.RsaPublicKey.fromCrock(meltCoin.coinPub).encode());
sessionHc.read((new native.Amount(meltAmount)).toNbo());
for (let j = 0; j < kappa; j++) {
let preCoins: RefreshPreCoin[] = [];
for (let i = 0; i < newCoinDenoms.length; i++) {
let coinPriv = native.EddsaPrivateKey.create();
let coinPub = coinPriv.getPublicKey();
let blindingFactor = native.RsaBlindingKeySecret.create();
let pubHash: native.HashCode = coinPub.hash();
let denomPub = native.RsaPublicKey.fromCrock(newCoinDenoms[i].denom_pub);
let ev: native.ByteArray = native.rsaBlind(pubHash,
blindingFactor,
denomPub);
let preCoin: RefreshPreCoin = {
blindingKey: blindingFactor.toCrock(),
coinEv: ev.toCrock(),
publicKey: coinPub.toCrock(),
privateKey: coinPriv.toCrock(),
};
preCoins.push(preCoin);
sessionHc.read(ev);
}
preCoinsForGammas.push(preCoins);
}
let sessionHash = new HashCode();
sessionHash.alloc();
sessionHc.finish(sessionHash);
let confirmData = new RefreshMeltCoinAffirmationPS({
coin_pub: EddsaPublicKey.fromCrock(meltCoin.coinPub),
amount_with_fee: (new Amount(meltAmount)).toNbo(),
session_hash: sessionHash,
melt_fee: (new Amount(meltFee)).toNbo()
});
let confirmSig: string = native.eddsaSign(confirmData.toPurpose(),
native.EddsaPrivateKey.fromCrock(
meltCoin.coinPriv)).toCrock();
let refreshSession: RefreshSession = {
meltCoinPub: meltCoin.coinPub,
newDenoms: newCoinDenoms.map((d) => d.denom_pub),
confirmSig,
valueWithFee: meltAmount,
transferPubs,
preCoinsForGammas,
};
return refreshSession;
}
}

View File

@ -25,7 +25,7 @@
*/ */
const DB_NAME = "taler"; const DB_NAME = "taler";
const DB_VERSION = 7; const DB_VERSION = 8;
/** /**
* Return a promise that resolves * Return a promise that resolves
@ -72,7 +72,7 @@ export function openTalerDb(): Promise<IDBDatabase> {
if (e.oldVersion != DB_VERSION) { if (e.oldVersion != DB_VERSION) {
window.alert("Incompatible wallet dababase version, please reset" + window.alert("Incompatible wallet dababase version, please reset" +
" db."); " db.");
chrome.browserAction.setBadgeText({text: "R!"}); chrome.browserAction.setBadgeText({text: "err"});
chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); chrome.browserAction.setBadgeBackgroundColor({color: "#F00"});
throw Error("incompatible DB"); throw Error("incompatible DB");
} }

View File

@ -119,10 +119,16 @@ var emscAlloc = {
['number', 'number', 'number', 'string']), ['number', 'number', 'number', 'string']),
eddsa_key_create: getEmsc('GNUNET_CRYPTO_eddsa_key_create', eddsa_key_create: getEmsc('GNUNET_CRYPTO_eddsa_key_create',
'number', []), 'number', []),
ecdsa_key_create: getEmsc('GNUNET_CRYPTO_ecdsa_key_create',
'number', []),
eddsa_public_key_from_private: getEmsc( eddsa_public_key_from_private: getEmsc(
'TALER_WRALL_eddsa_public_key_from_private', 'TALER_WRALL_eddsa_public_key_from_private',
'number', 'number',
['number']), ['number']),
ecdsa_public_key_from_private: getEmsc(
'TALER_WRALL_ecdsa_public_key_from_private',
'number',
['number']),
data_to_string_alloc: getEmsc('GNUNET_STRINGS_data_to_string_alloc', data_to_string_alloc: getEmsc('GNUNET_STRINGS_data_to_string_alloc',
'number', 'number',
['number', 'number']), ['number', 'number']),
@ -181,7 +187,7 @@ interface ArenaObject {
} }
class HashContext implements ArenaObject { export class HashContext implements ArenaObject {
private hashContextPtr: number | undefined; private hashContextPtr: number | undefined;
constructor() { constructor() {
@ -590,6 +596,29 @@ export class EddsaPrivateKey extends PackedArenaObject {
mixinStatic(EddsaPrivateKey, fromCrock); mixinStatic(EddsaPrivateKey, fromCrock);
export class EcdsaPrivateKey extends PackedArenaObject {
static create(a?: Arena): EcdsaPrivateKey {
let obj = new EcdsaPrivateKey(a);
obj.nativePtr = emscAlloc.ecdsa_key_create();
return obj;
}
size() {
return 32;
}
getPublicKey(a?: Arena): EcdsaPublicKey {
let obj = new EcdsaPublicKey(a);
obj.nativePtr = emscAlloc.ecdsa_public_key_from_private(this.nativePtr);
return obj;
}
static fromCrock: (s: string) => EcdsaPrivateKey;
}
mixinStatic(EcdsaPrivateKey, fromCrock);
function fromCrock(s: string) { function fromCrock(s: string) {
let x = new this(); let x = new this();
x.alloc(); x.alloc();
@ -629,6 +658,16 @@ export class EddsaPublicKey extends PackedArenaObject {
} }
mixinStatic(EddsaPublicKey, fromCrock); mixinStatic(EddsaPublicKey, fromCrock);
export class EcdsaPublicKey extends PackedArenaObject {
size() {
return 32;
}
static fromCrock: (s: string) => EcdsaPublicKey;
}
mixinStatic(EddsaPublicKey, fromCrock);
function makeFromCrock(decodeFn: (p: number, s: number) => number) { function makeFromCrock(decodeFn: (p: number, s: number) => number) {
function fromCrock(s: string, a?: Arena) { function fromCrock(s: string, a?: Arena) {
let obj = new this(a); let obj = new this(a);

View File

@ -24,10 +24,6 @@
"use strict"; "use strict";
export function Query(db: IDBDatabase) {
return new QueryRoot(db);
}
/** /**
* Stream that can be filtered, reduced or joined * Stream that can be filtered, reduced or joined
* with indices. * with indices.
@ -265,7 +261,7 @@ class IterQueryStream<T> extends QueryStreamBase<T> {
} }
class QueryRoot { export class QueryRoot {
private work: ((t: IDBTransaction) => void)[] = []; private work: ((t: IDBTransaction) => void)[] = [];
private db: IDBDatabase; private db: IDBDatabase;
private stores = new Set(); private stores = new Set();
@ -332,7 +328,7 @@ class QueryRoot {
/** /**
* Get one object from a store by its key. * Get one object from a store by its key.
*/ */
get(storeName: any, key: any): Promise<any> { get<T>(storeName: any, key: any): Promise<T|undefined> {
if (key === void 0) { if (key === void 0) {
throw Error("key must not be undefined"); throw Error("key must not be undefined");
} }

View File

@ -42,6 +42,30 @@ export class AmountJson {
} }
export 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 @Checkable.Class
export class CreateReserveResponse { export class CreateReserveResponse {
/** /**
@ -147,6 +171,13 @@ export interface PreCoin {
coinValue: AmountJson; coinValue: AmountJson;
} }
export interface RefreshPreCoin {
publicKey: string;
privateKey: string;
coinEv: string;
blindingKey: string
}
/** /**
* Ongoing refresh * Ongoing refresh
@ -173,20 +204,9 @@ export interface RefreshSession {
*/ */
newDenoms: string[]; newDenoms: string[];
/**
* Blinded public keys for the requested coins.
*/
newCoinBlanks: string[][];
/** preCoinsForGammas: RefreshPreCoin[][];
* Blinding factors for the new coins.
*/
newCoinBlindingFactors: string[][];
/**
* Private keys for the requested coins.
*/
newCoinPrivs: string[][];
/** /**
* The transfer keys, kappa of them. * The transfer keys, kappa of them.
@ -195,15 +215,6 @@ export interface RefreshSession {
} }
export interface Reserve {
exchange_base_url: string
reserve_priv: string;
reserve_pub: string;
created: number;
current_amount: AmountJson;
}
export interface CoinPaySig { export interface CoinPaySig {
coin_sig: string; coin_sig: string;
coin_pub: string; coin_pub: string;

View File

@ -27,21 +27,20 @@ import {
IExchangeInfo, IExchangeInfo,
Denomination, Denomination,
Notifier, Notifier,
WireInfo WireInfo, RefreshSession, ReserveRecord
} from "./types"; } from "./types";
import { HttpResponse, RequestException } from "./http"; import {HttpResponse, RequestException} from "./http";
import { Query } from "./query"; import {QueryRoot} from "./query";
import { Checkable } from "./checkable"; import {Checkable} from "./checkable";
import { canonicalizeBaseUrl } from "./helpers"; import {canonicalizeBaseUrl} from "./helpers";
import { ReserveCreationInfo, Amounts } from "./types"; import {ReserveCreationInfo, Amounts} from "./types";
import { PreCoin } from "./types"; import {PreCoin} from "./types";
import { Reserve } from "./types"; import {CryptoApi} from "./cryptoApi";
import { CryptoApi } from "./cryptoApi"; import {Coin} from "./types";
import { Coin } from "./types"; import {PayCoinInfo} from "./types";
import { PayCoinInfo } from "./types"; import {CheckRepurchaseResult} from "./types";
import { CheckRepurchaseResult } from "./types"; import {Contract} from "./types";
import { Contract } from "./types"; import {ExchangeHandle} from "./types";
import { ExchangeHandle } from "./types";
"use strict"; "use strict";
@ -50,29 +49,6 @@ export interface CoinWithDenom {
denom: Denomination; 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 @Checkable.Class
export class KeysJson { export class KeysJson {
@ -256,8 +232,8 @@ function isWithdrawableDenom(d: Denomination) {
interface HttpRequestLibrary { interface HttpRequestLibrary {
req(method: string, req(method: string,
url: string | uri.URI, url: string | uri.URI,
options?: any): Promise<HttpResponse>; options?: any): Promise<HttpResponse>;
get(url: string | uri.URI): Promise<HttpResponse>; get(url: string | uri.URI): Promise<HttpResponse>;
@ -288,7 +264,7 @@ interface KeyUpdateInfo {
* amount, but never larger. * amount, but never larger.
*/ */
function getWithdrawDenomList(amountAvailable: AmountJson, function getWithdrawDenomList(amountAvailable: AmountJson,
denoms: Denomination[]): Denomination[] { denoms: Denomination[]): Denomination[] {
let remaining = Amounts.copy(amountAvailable); let remaining = Amounts.copy(amountAvailable);
const ds: Denomination[] = []; const ds: Denomination[] = [];
@ -330,10 +306,14 @@ export class Wallet {
*/ */
private runningOperations: Set<string> = new Set(); private runningOperations: Set<string> = new Set();
q(): QueryRoot {
return new QueryRoot(this.db);
}
constructor(db: IDBDatabase, constructor(db: IDBDatabase,
http: HttpRequestLibrary, http: HttpRequestLibrary,
badge: Badge, badge: Badge,
notifier: Notifier) { notifier: Notifier) {
this.db = db; this.db = db;
this.http = http; this.http = http;
this.badge = badge; this.badge = badge;
@ -359,14 +339,14 @@ export class Wallet {
updateExchanges(): void { updateExchanges(): void {
console.log("updating exchanges"); console.log("updating exchanges");
Query(this.db) this.q()
.iter("exchanges") .iter("exchanges")
.reduce((exchange: IExchangeInfo) => { .reduce((exchange: IExchangeInfo) => {
this.updateExchangeFromUrl(exchange.baseUrl) this.updateExchangeFromUrl(exchange.baseUrl)
.catch((e) => { .catch((e) => {
console.error("updating exchange failed", e); console.error("updating exchange failed", e);
}); });
}); });
} }
/** /**
@ -376,19 +356,19 @@ export class Wallet {
private resumePendingFromDb(): void { private resumePendingFromDb(): void {
console.log("resuming pending operations from db"); console.log("resuming pending operations from db");
Query(this.db) this.q()
.iter("reserves") .iter("reserves")
.reduce((reserve: any) => { .reduce((reserve: any) => {
console.log("resuming reserve", reserve.reserve_pub); console.log("resuming reserve", reserve.reserve_pub);
this.processReserve(reserve); this.processReserve(reserve);
}); });
Query(this.db) this.q()
.iter("precoins") .iter("precoins")
.reduce((preCoin: any) => { .reduce((preCoin: any) => {
console.log("resuming precoin"); console.log("resuming precoin");
this.processPreCoin(preCoin); this.processPreCoin(preCoin);
}); });
} }
@ -397,8 +377,8 @@ export class Wallet {
* but only if the sum the coins' remaining value exceeds the payment amount. * but only if the sum the coins' remaining value exceeds the payment amount.
*/ */
private async getPossibleExchangeCoins(paymentAmount: AmountJson, private async getPossibleExchangeCoins(paymentAmount: AmountJson,
depositFeeLimit: AmountJson, depositFeeLimit: AmountJson,
allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> { allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> {
// Mapping from exchange base URL to list of coins together with their // Mapping from exchange base URL to list of coins together with their
// denomination // denomination
let m: ExchangeCoins = {}; let m: ExchangeCoins = {};
@ -411,9 +391,9 @@ export class Wallet {
let coin: Coin = mc[1]; let coin: Coin = mc[1];
if (coin.suspended) { if (coin.suspended) {
console.log("skipping suspended coin", console.log("skipping suspended coin",
coin.denomPub, coin.denomPub,
"from exchange", "from exchange",
exchange.baseUrl); exchange.baseUrl);
return; return;
} }
let denom = exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub); let denom = exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub);
@ -425,7 +405,7 @@ export class Wallet {
console.warn("same pubkey for different currencies"); console.warn("same pubkey for different currencies");
return; return;
} }
let cd = { coin, denom }; let cd = {coin, denom};
let x = m[url]; let x = m[url];
if (!x) { if (!x) {
m[url] = [cd]; m[url] = [cd];
@ -445,10 +425,12 @@ export class Wallet {
handledExchanges.add(info.url); handledExchanges.add(info.url);
console.log("Checking for merchant's exchange", JSON.stringify(info)); console.log("Checking for merchant's exchange", JSON.stringify(info));
return [ return [
Query(this.db) this.q()
.iter("exchanges", { indexName: "pubKey", only: info.master_pub }) .iter("exchanges", {indexName: "pubKey", only: info.master_pub})
.indexJoin("coins", "exchangeBaseUrl", (exchange) => exchange.baseUrl) .indexJoin("coins",
.reduce((x) => storeExchangeCoin(x, info.url)) "exchangeBaseUrl",
(exchange) => exchange.baseUrl)
.reduce((x) => storeExchangeCoin(x, info.url))
]; ];
}); });
@ -467,38 +449,38 @@ export class Wallet {
// under depositFeeLimit // under depositFeeLimit
nextExchange: nextExchange:
for (let key in m) { for (let key in m) {
let coins = m[key]; let coins = m[key];
// Sort by ascending deposit fee // Sort by ascending deposit fee
coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit, coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit,
o2.denom.fee_deposit)); o2.denom.fee_deposit));
let maxFee = Amounts.copy(depositFeeLimit); let maxFee = Amounts.copy(depositFeeLimit);
let minAmount = Amounts.copy(paymentAmount); let minAmount = Amounts.copy(paymentAmount);
let accFee = Amounts.copy(coins[0].denom.fee_deposit); let accFee = Amounts.copy(coins[0].denom.fee_deposit);
let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency); let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency);
let usableCoins: CoinWithDenom[] = []; let usableCoins: CoinWithDenom[] = [];
nextCoin: nextCoin:
for (let i = 0; i < coins.length; i++) { for (let i = 0; i < coins.length; i++) {
let coinAmount = Amounts.copy(coins[i].coin.currentAmount); let coinAmount = Amounts.copy(coins[i].coin.currentAmount);
let coinFee = coins[i].denom.fee_deposit; let coinFee = coins[i].denom.fee_deposit;
if (Amounts.cmp(coinAmount, coinFee) <= 0) { if (Amounts.cmp(coinAmount, coinFee) <= 0) {
continue nextCoin; continue nextCoin;
} }
accFee = Amounts.add(accFee, coinFee).amount; accFee = Amounts.add(accFee, coinFee).amount;
accAmount = Amounts.add(accAmount, coinAmount).amount; accAmount = Amounts.add(accAmount, coinAmount).amount;
if (Amounts.cmp(accFee, maxFee) >= 0) { if (Amounts.cmp(accFee, maxFee) >= 0) {
// FIXME: if the fees are too high, we have // FIXME: if the fees are too high, we have
// to cover them ourselves .... // to cover them ourselves ....
console.log("too much fees"); console.log("too much fees");
continue nextExchange; continue nextExchange;
} }
usableCoins.push(coins[i]); usableCoins.push(coins[i]);
if (Amounts.cmp(accAmount, minAmount) >= 0) { if (Amounts.cmp(accAmount, minAmount) >= 0) {
ret[key] = usableCoins; ret[key] = usableCoins;
continue nextExchange; continue nextExchange;
} }
}
} }
}
return ret; return ret;
} }
@ -508,8 +490,8 @@ export class Wallet {
* pay for a contract in the wallet's database. * pay for a contract in the wallet's database.
*/ */
private async recordConfirmPay(offer: Offer, private async recordConfirmPay(offer: Offer,
payCoinInfo: PayCoinInfo, payCoinInfo: PayCoinInfo,
chosenExchange: string): Promise<void> { chosenExchange: string): Promise<void> {
let payReq: any = {}; let payReq: any = {};
payReq["amount"] = offer.contract.amount; payReq["amount"] = offer.contract.amount;
payReq["coins"] = payCoinInfo.map((x) => x.sig); payReq["coins"] = payCoinInfo.map((x) => x.sig);
@ -539,18 +521,18 @@ export class Wallet {
} }
}; };
await Query(this.db) await this.q()
.put("transactions", t) .put("transactions", t)
.put("history", historyEntry) .put("history", historyEntry)
.putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin))
.finish(); .finish();
this.notifier.notify(); this.notifier.notify();
} }
async putHistory(historyEntry: HistoryRecord): Promise<void> { async putHistory(historyEntry: HistoryRecord): Promise<void> {
await Query(this.db).put("history", historyEntry).finish(); await this.q().put("history", historyEntry).finish();
this.notifier.notify(); this.notifier.notify();
} }
@ -562,8 +544,7 @@ export class Wallet {
async confirmPay(offer: Offer): Promise<any> { async confirmPay(offer: Offer): Promise<any> {
console.log("executing confirmPay"); console.log("executing confirmPay");
let transaction = await Query(this.db) let transaction = await this.q().get("transactions", offer.H_contract);
.get("transactions", offer.H_contract);
if (transaction) { if (transaction) {
// Already payed ... // Already payed ...
@ -571,8 +552,8 @@ export class Wallet {
} }
let mcs = await this.getPossibleExchangeCoins(offer.contract.amount, let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
offer.contract.max_fee, offer.contract.max_fee,
offer.contract.exchanges); offer.contract.exchanges);
if (Object.keys(mcs).length == 0) { if (Object.keys(mcs).length == 0) {
console.log("not confirming payment, insufficient coins"); console.log("not confirming payment, insufficient coins");
@ -584,8 +565,8 @@ export class Wallet {
let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]); let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]);
await this.recordConfirmPay(offer, await this.recordConfirmPay(offer,
ds, ds,
exchangeUrl); exchangeUrl);
return {}; return {};
} }
@ -596,17 +577,15 @@ export class Wallet {
*/ */
async checkPay(offer: Offer): Promise<any> { async checkPay(offer: Offer): Promise<any> {
// First check if we already payed for it. // First check if we already payed for it.
let transaction = await let transaction = await this.q().get("transactions", offer.H_contract);
Query(this.db)
.get("transactions", offer.H_contract);
if (transaction) { if (transaction) {
return { isPayed: true }; return {isPayed: true};
} }
// If not already payed, check if we could pay for it. // If not already payed, check if we could pay for it.
let mcs = await this.getPossibleExchangeCoins(offer.contract.amount, let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
offer.contract.max_fee, offer.contract.max_fee,
offer.contract.exchanges); offer.contract.exchanges);
if (Object.keys(mcs).length == 0) { if (Object.keys(mcs).length == 0) {
console.log("not confirming payment, insufficient coins"); console.log("not confirming payment, insufficient coins");
@ -614,7 +593,7 @@ export class Wallet {
error: "coins-insufficient", error: "coins-insufficient",
}; };
} }
return { isPayed: false }; return {isPayed: false};
} }
@ -623,8 +602,7 @@ export class Wallet {
* with the given hash. * with the given hash.
*/ */
async executePayment(H_contract: string): Promise<any> { async executePayment(H_contract: string): Promise<any> {
let t = await Query(this.db) let t = await this.q().get<Transaction>("transactions", H_contract);
.get("transactions", H_contract);
if (!t) { if (!t) {
return { return {
success: false, success: false,
@ -645,14 +623,14 @@ export class Wallet {
* then deplete the reserve, withdrawing coins until it is empty. * then deplete the reserve, withdrawing coins until it is empty.
*/ */
private async processReserve(reserveRecord: ReserveRecord, private async processReserve(reserveRecord: ReserveRecord,
retryDelayMs: number = 250): Promise<void> { retryDelayMs: number = 250): Promise<void> {
const opId = "reserve-" + reserveRecord.reserve_pub; const opId = "reserve-" + reserveRecord.reserve_pub;
this.startOperation(opId); this.startOperation(opId);
try { try {
let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url); let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
let reserve = await this.updateReserve(reserveRecord.reserve_pub, let reserve = await this.updateReserve(reserveRecord.reserve_pub,
exchange); exchange);
let n = await this.depleteReserve(reserve, exchange); let n = await this.depleteReserve(reserve, exchange);
if (n != 0) { if (n != 0) {
@ -667,15 +645,15 @@ export class Wallet {
currentAmount: reserveRecord.current_amount, currentAmount: reserveRecord.current_amount,
} }
}; };
await Query(this.db).put("history", depleted).finish(); await this.q().put("history", depleted).finish();
} }
} catch (e) { } catch (e) {
// random, exponential backoff truncated at 3 minutes // random, exponential backoff truncated at 3 minutes
let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(),
3000 * 60); 3000 * 60);
console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`); console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`);
setTimeout(() => this.processReserve(reserveRecord, nextDelay), setTimeout(() => this.processReserve(reserveRecord, nextDelay),
retryDelayMs); retryDelayMs);
} finally { } finally {
this.stopOperation(opId); this.stopOperation(opId);
} }
@ -683,18 +661,18 @@ export class Wallet {
private async processPreCoin(preCoin: PreCoin, private async processPreCoin(preCoin: PreCoin,
retryDelayMs = 100): Promise<void> { retryDelayMs = 100): Promise<void> {
try { try {
const coin = await this.withdrawExecute(preCoin); const coin = await this.withdrawExecute(preCoin);
this.storeCoin(coin); this.storeCoin(coin);
} catch (e) { } catch (e) {
console.error("Failed to withdraw coin from precoin, retrying in", console.error("Failed to withdraw coin from precoin, retrying in",
retryDelayMs, retryDelayMs,
"ms", e); "ms", e);
// exponential backoff truncated at one minute // exponential backoff truncated at one minute
let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60); let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60);
setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs), setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs),
retryDelayMs); retryDelayMs);
} }
} }
@ -730,10 +708,10 @@ export class Wallet {
} }
}; };
await Query(this.db) await this.q()
.put("reserves", reserveRecord) .put("reserves", reserveRecord)
.put("history", historyEntry) .put("history", historyEntry)
.finish(); .finish();
let r: CreateReserveResponse = { let r: CreateReserveResponse = {
exchange: canonExchange, exchange: canonExchange,
@ -754,8 +732,13 @@ export class Wallet {
*/ */
async confirmReserve(req: ConfirmReserveRequest): Promise<void> { async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
const now = (new Date).getTime(); const now = (new Date).getTime();
let reserve: ReserveRecord = await Query(this.db) let reserve: ReserveRecord|undefined = await (
.get("reserves", req.reservePub); this.q().get<ReserveRecord>("reserves",
req.reservePub));
if (!reserve) {
console.error("Unable to confirm reserve, not found in DB");
return;
}
const historyEntry = { const historyEntry = {
type: "confirm-reserve", type: "confirm-reserve",
timestamp: now, timestamp: now,
@ -766,23 +749,22 @@ export class Wallet {
requestedAmount: reserve.requested_amount, requestedAmount: reserve.requested_amount,
} }
}; };
if (!reserve) {
console.error("Unable to confirm reserve, not found in DB");
return;
}
reserve.confirmed = true; reserve.confirmed = true;
await Query(this.db) await this.q()
.put("reserves", reserve) .put("reserves", reserve)
.put("history", historyEntry) .put("history", historyEntry)
.finish(); .finish();
this.processReserve(reserve); this.processReserve(reserve);
} }
private async withdrawExecute(pc: PreCoin): Promise<Coin> { private async withdrawExecute(pc: PreCoin): Promise<Coin> {
let reserve = await Query(this.db) let reserve = await this.q().get<ReserveRecord>("reserves", pc.reservePub);
.get("reserves", pc.reservePub);
if (!reserve) {
throw Error("db inconsistent");
}
let wd: any = {}; let wd: any = {};
wd.denom_pub = pc.denomPub; wd.denom_pub = pc.denomPub;
@ -801,8 +783,8 @@ export class Wallet {
} }
let r = JSON.parse(resp.responseText); let r = JSON.parse(resp.responseText);
let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig,
pc.blindingKey, pc.blindingKey,
pc.denomPub); pc.denomPub);
let coin: Coin = { let coin: Coin = {
coinPub: pc.coinPub, coinPub: pc.coinPub,
coinPriv: pc.coinPriv, coinPriv: pc.coinPriv,
@ -825,11 +807,11 @@ export class Wallet {
coinPub: coin.coinPub, coinPub: coin.coinPub,
} }
}; };
await Query(this.db) await this.q()
.delete("precoins", coin.coinPub) .delete("precoins", coin.coinPub)
.add("coins", coin) .add("coins", coin)
.add("history", historyEntry) .add("history", historyEntry)
.finish(); .finish();
this.notifier.notify(); this.notifier.notify();
} }
@ -837,13 +819,14 @@ export class Wallet {
/** /**
* Withdraw one coin of the given denomination from the given reserve. * Withdraw one coin of the given denomination from the given reserve.
*/ */
private async withdraw(denom: Denomination, reserve: Reserve): Promise<void> { private async withdraw(denom: Denomination,
reserve: ReserveRecord): Promise<void> {
console.log("creating pre coin at", new Date()); console.log("creating pre coin at", new Date());
let preCoin = await this.cryptoApi let preCoin = await this.cryptoApi
.createPreCoin(denom, reserve); .createPreCoin(denom, reserve);
await Query(this.db) await this.q()
.put("precoins", preCoin) .put("precoins", preCoin)
.finish(); .finish();
await this.processPreCoin(preCoin); await this.processPreCoin(preCoin);
} }
@ -852,10 +835,10 @@ export class Wallet {
* Withdraw coins from a reserve until it is empty. * Withdraw coins from a reserve until it is empty.
*/ */
private async depleteReserve(reserve: any, private async depleteReserve(reserve: any,
exchange: IExchangeInfo): Promise<number> { exchange: IExchangeInfo): Promise<number> {
let denomsAvailable: Denomination[] = copy(exchange.active_denoms); let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
denomsAvailable); denomsAvailable);
let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve)); let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve));
await Promise.all(ps); await Promise.all(ps);
@ -868,11 +851,14 @@ export class Wallet {
* by quering the reserve's exchange. * by quering the reserve's exchange.
*/ */
private async updateReserve(reservePub: string, private async updateReserve(reservePub: string,
exchange: IExchangeInfo): Promise<Reserve> { exchange: IExchangeInfo): Promise<ReserveRecord> {
let reserve = await Query(this.db) let reserve = await this.q()
.get("reserves", reservePub); .get<ReserveRecord>("reserves", reservePub);
if (!reserve) {
throw Error("reserve not in db");
}
let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl); let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl);
reqUrl.query({ 'reserve_pub': reservePub }); reqUrl.query({'reserve_pub': reservePub});
let resp = await this.http.get(reqUrl); let resp = await this.http.get(reqUrl);
if (resp.status != 200) { if (resp.status != 200) {
throw Error(); throw Error();
@ -895,9 +881,9 @@ export class Wallet {
newAmount newAmount
} }
}; };
await Query(this.db) await this.q()
.put("reserves", reserve) .put("reserves", reserve)
.finish(); .finish();
return reserve; return reserve;
} }
@ -922,18 +908,18 @@ export class Wallet {
} }
async getReserveCreationInfo(baseUrl: string, async getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> { amount: AmountJson): Promise<ReserveCreationInfo> {
let exchangeInfo = await this.updateExchangeFromUrl(baseUrl); let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
let selectedDenoms = getWithdrawDenomList(amount, let selectedDenoms = getWithdrawDenomList(amount,
exchangeInfo.active_denoms); exchangeInfo.active_denoms);
let acc = Amounts.getZero(amount.currency); let acc = Amounts.getZero(amount.currency);
for (let d of selectedDenoms) { for (let d of selectedDenoms) {
acc = Amounts.add(acc, d.fee_withdraw).amount; acc = Amounts.add(acc, d.fee_withdraw).amount;
} }
let actualCoinCost = selectedDenoms let actualCoinCost = selectedDenoms
.map((d: Denomination) => Amounts.add(d.value, .map((d: Denomination) => Amounts.add(d.value,
d.fee_withdraw).amount) d.fee_withdraw).amount)
.reduce((a, b) => Amounts.add(a, b).amount); .reduce((a, b) => Amounts.add(a, b).amount);
let wireInfo = await this.getWireInfo(baseUrl); let wireInfo = await this.getWireInfo(baseUrl);
@ -966,17 +952,18 @@ export class Wallet {
} }
private async suspendCoins(exchangeInfo: IExchangeInfo): Promise<void> { private async suspendCoins(exchangeInfo: IExchangeInfo): Promise<void> {
let suspendedCoins = await Query(this.db) let suspendedCoins = await (
.iter("coins", this.q()
{ indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl }) .iter("coins",
.reduce((coin: Coin, suspendedCoins: Coin[]) => { {indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl})
if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { .reduce((coin: Coin, suspendedCoins: Coin[]) => {
return Array.prototype.concat(suspendedCoins, [coin]); if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) {
} return Array.prototype.concat(suspendedCoins, [coin]);
return Array.prototype.concat(suspendedCoins); }
}, []); return Array.prototype.concat(suspendedCoins);
}, []));
let q = Query(this.db); let q = this.q();
suspendedCoins.map((c) => { suspendedCoins.map((c) => {
console.log("suspending coin", c); console.log("suspending coin", c);
c.suspended = true; c.suspended = true;
@ -987,13 +974,13 @@ export class Wallet {
private async updateExchangeFromJson(baseUrl: string, private async updateExchangeFromJson(baseUrl: string,
exchangeKeysJson: KeysJson): Promise<IExchangeInfo> { exchangeKeysJson: KeysJson): Promise<IExchangeInfo> {
const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
if (updateTimeSec === null) { if (updateTimeSec === null) {
throw Error("invalid update time"); throw Error("invalid update time");
} }
let r = await Query(this.db).get("exchanges", baseUrl); let r = await this.q().get<IExchangeInfo>("exchanges", baseUrl);
let exchangeInfo: IExchangeInfo; let exchangeInfo: IExchangeInfo;
@ -1016,19 +1003,19 @@ export class Wallet {
} }
let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo, let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo,
exchangeKeysJson); exchangeKeysJson);
await this.suspendCoins(updatedExchangeInfo); await this.suspendCoins(updatedExchangeInfo);
await Query(this.db) await this.q()
.put("exchanges", updatedExchangeInfo) .put("exchanges", updatedExchangeInfo)
.finish(); .finish();
return updatedExchangeInfo; return updatedExchangeInfo;
} }
private async updateExchangeInfo(exchangeInfo: IExchangeInfo, private async updateExchangeInfo(exchangeInfo: IExchangeInfo,
newKeys: KeysJson): Promise<IExchangeInfo> { newKeys: KeysJson): Promise<IExchangeInfo> {
if (exchangeInfo.masterPublicKey != newKeys.master_public_key) { if (exchangeInfo.masterPublicKey != newKeys.master_public_key) {
throw Error("public keys do not match"); throw Error("public keys do not match");
} }
@ -1064,15 +1051,15 @@ export class Wallet {
return true; return true;
}); });
let ps = denomsToCheck.map(async (denom) => { let ps = denomsToCheck.map(async(denom) => {
let valid = await this.cryptoApi let valid = await this.cryptoApi
.isValidDenom(denom, .isValidDenom(denom,
exchangeInfo.masterPublicKey); exchangeInfo.masterPublicKey);
if (!valid) { if (!valid) {
console.error("invalid denomination", console.error("invalid denomination",
denom, denom,
"with key", "with key",
exchangeInfo.masterPublicKey); exchangeInfo.masterPublicKey);
// FIXME: report to auditors // FIXME: report to auditors
} }
exchangeInfo.active_denoms.push(denom); exchangeInfo.active_denoms.push(denom);
@ -1099,15 +1086,58 @@ export class Wallet {
acc = Amounts.getZero(c.currentAmount.currency); acc = Amounts.getZero(c.currentAmount.currency);
} }
byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount, byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount,
acc).amount; acc).amount;
return byCurrency; return byCurrency;
} }
let byCurrency = await Query(this.db) let byCurrency = await (
.iter("coins") this.q()
.reduce(collectBalances, {}); .iter("coins")
.reduce(collectBalances, {}));
return {balances: byCurrency};
}
async refresh(oldCoinPub: string): Promise<void> {
// FIXME: this is not running in a transaction.
let coin = await this.q().get<Coin>("coins", oldCoinPub);
if (!coin) {
console.error("coin not found");
return;
}
let exchange = await this.q().get<IExchangeInfo>("exchanges",
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 newCoinDenoms = getWithdrawDenomList(coin.currentAmount,
availableDenoms);
console.log("refreshing into", newCoinDenoms);
let refreshSession: RefreshSession = await (
this.cryptoApi.createWithdrawSession(3,
coin,
newCoinDenoms,
coin.currentAmount,
oldDenom.fee_refresh));
// FIXME: implement rest
return { balances: byCurrency };
} }
@ -1120,40 +1150,40 @@ export class Wallet {
return acc; return acc;
} }
let history = await let history = await (
Query(this.db) this.q()
.iter("history", { indexName: "timestamp" }) .iter("history", {indexName: "timestamp"})
.reduce(collect, []); .reduce(collect, []));
return { history }; return {history};
} }
async getExchanges(): Promise<IExchangeInfo[]> { async getExchanges(): Promise<IExchangeInfo[]> {
return Query(this.db) return this.q()
.iter<IExchangeInfo>("exchanges") .iter<IExchangeInfo>("exchanges")
.flatMap((e) => [e]) .flatMap((e) => [e])
.toArray(); .toArray();
} }
async getReserves(exchangeBaseUrl: string): Promise<Reserve[]> { async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
return Query(this.db) return this.q()
.iter<Reserve>("reserves") .iter<ReserveRecord>("reserves")
.filter((r: Reserve) => r.exchange_base_url === exchangeBaseUrl) .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl)
.toArray(); .toArray();
} }
async getCoins(exchangeBaseUrl: string): Promise<Coin[]> { async getCoins(exchangeBaseUrl: string): Promise<Coin[]> {
return Query(this.db) return this.q()
.iter<Coin>("coins") .iter<Coin>("coins")
.filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl) .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl)
.toArray(); .toArray();
} }
async getPreCoins(exchangeBaseUrl: string): Promise<PreCoin[]> { async getPreCoins(exchangeBaseUrl: string): Promise<PreCoin[]> {
return Query(this.db) return this.q()
.iter<PreCoin>("precoins") .iter<PreCoin>("precoins")
.filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl) .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl)
.toArray(); .toArray();
} }
@ -1167,12 +1197,16 @@ export class Wallet {
async checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> { async checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> {
if (!contract.repurchase_correlation_id) { if (!contract.repurchase_correlation_id) {
console.log("no repurchase: no correlation id"); console.log("no repurchase: no correlation id");
return { isRepurchase: false }; return {isRepurchase: false};
} }
let result: Transaction = await Query(this.db) let result: Transaction = await (
.getIndexed("transactions", this.q()
"repurchase", .getIndexed("transactions",
[contract.merchant_pub, contract.repurchase_correlation_id]); "repurchase",
[
contract.merchant_pub,
contract.repurchase_correlation_id
]));
if (result) { if (result) {
console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id); console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id);
@ -1182,7 +1216,7 @@ export class Wallet {
existingFulfillmentUrl: result.contract.fulfillment_url, existingFulfillmentUrl: result.contract.fulfillment_url,
}; };
} else { } else {
return { isRepurchase: false }; return {isRepurchase: false};
} }
} }
} }

View File

@ -20,7 +20,7 @@ import {
PreCoin, PreCoin,
ReserveCreationInfo, ReserveCreationInfo,
IExchangeInfo, IExchangeInfo,
Reserve ReserveRecord
} from "./types"; } from "./types";
/** /**
@ -58,7 +58,7 @@ export async function getExchanges(): Promise<IExchangeInfo[]> {
return await callBackend("get-exchanges"); return await callBackend("get-exchanges");
} }
export async function getReserves(exchangeBaseUrl: string): Promise<Reserve[]> { export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
return await callBackend("get-reserves", { exchangeBaseUrl }); return await callBackend("get-reserves", { exchangeBaseUrl });
} }

View File

@ -23,18 +23,18 @@
/// <reference path="../lib/decl/preact.d.ts" /> /// <reference path="../lib/decl/preact.d.ts" />
import { IExchangeInfo } from "../lib/wallet/types"; import { IExchangeInfo } from "../lib/wallet/types";
import { Reserve, Coin, PreCoin, Denomination } from "../lib/wallet/types"; import { ReserveRecord, Coin, PreCoin, Denomination } from "../lib/wallet/types";
import { ImplicitStateComponent, StateHolder } from "../lib/components"; import { ImplicitStateComponent, StateHolder } from "../lib/components";
import { getReserves, getExchanges, getCoins, getPreCoins } from "../lib/wallet/wxApi"; import { getReserves, getExchanges, getCoins, getPreCoins } from "../lib/wallet/wxApi";
import { prettyAmount, abbrev } from "../lib/wallet/renderHtml"; import { prettyAmount, abbrev } from "../lib/wallet/renderHtml";
interface ReserveViewProps { interface ReserveViewProps {
reserve: Reserve; reserve: ReserveRecord;
} }
class ReserveView extends preact.Component<ReserveViewProps, void> { class ReserveView extends preact.Component<ReserveViewProps, void> {
render(): JSX.Element { render(): JSX.Element {
let r: Reserve = this.props.reserve; let r: ReserveRecord = this.props.reserve;
return ( return (
<div className="tree-item"> <div className="tree-item">
<ul> <ul>
@ -248,7 +248,7 @@ class DenominationList extends ImplicitStateComponent<DenominationListProps> {
} }
class ReserveList extends ImplicitStateComponent<ReserveListProps> { class ReserveList extends ImplicitStateComponent<ReserveListProps> {
reserves = this.makeState<Reserve[] | null>(null); reserves = this.makeState<ReserveRecord[] | null>(null);
expanded = this.makeState<boolean>(false); expanded = this.makeState<boolean>(false);
constructor(props: ReserveListProps) { constructor(props: ReserveListProps) {