From 89c3c2d58d1a7102a10edfb716cbc7434d0bc117 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 18 Oct 2016 01:16:31 +0200 Subject: [PATCH] make db layer more type safe --- content_scripts/notify.ts | 21 ++++-- lib/wallet/db.ts | 2 + lib/wallet/query.ts | 40 ++++++----- lib/wallet/wallet.ts | 137 +++++++++++++++++++++----------------- 4 files changed, 117 insertions(+), 83 deletions(-) diff --git a/content_scripts/notify.ts b/content_scripts/notify.ts index 959c0e557..3b7b231c3 100644 --- a/content_scripts/notify.ts +++ b/content_scripts/notify.ts @@ -34,6 +34,13 @@ declare var cloneInto: any; namespace TalerNotify { const PROTOCOL_VERSION = 1; + let logVerbose: boolean = false; + try { + logVerbose = !!localStorage.getItem("taler-log-verbose"); + } catch (e) { + // can't read from local storage + } + if (!taler) { console.error("Taler wallet lib not included, HTTP 402 payments not" + " supported"); @@ -98,7 +105,7 @@ namespace TalerNotify { function init() { chrome.runtime.sendMessage({type: "get-tab-cookie"}, (resp) => { if (chrome.runtime.lastError) { - console.log("extension not yet ready"); + logVerbose && console.log("extension not yet ready"); window.setTimeout(init, 200); return; } @@ -107,19 +114,19 @@ namespace TalerNotify { let port = chrome.runtime.connect(); port.onDisconnect.addListener(() => { - console.log("chrome runtime disconnected, removing handlers"); + logVerbose && console.log("chrome runtime disconnected, removing handlers"); for (let handler of handlers) { document.removeEventListener(handler.type, handler.listener); } }); if (resp && resp.type === "fetch") { - console.log("it's fetch"); + logVerbose && console.log("it's fetch"); taler.internalOfferContractFrom(resp.contractUrl); document.documentElement.style.visibility = "hidden"; } else if (resp && resp.type === "execute") { - console.log("it's execute"); + logVerbose && console.log("it's execute"); document.documentElement.style.visibility = "hidden"; taler.internalExecutePayment(resp.contractHash, resp.payUrl, @@ -128,7 +135,7 @@ namespace TalerNotify { }); } - console.log("loading Taler content script"); + logVerbose && console.log("loading Taler content script"); init(); interface HandlerFn { @@ -238,7 +245,7 @@ namespace TalerNotify { } if (resp.isRepurchase) { - console.log("doing repurchase"); + logVerbose && console.log("doing repurchase"); console.assert(resp.existingFulfillmentUrl); console.assert(resp.existingContractHash); window.location.href = subst(resp.existingFulfillmentUrl, @@ -296,7 +303,7 @@ namespace TalerNotify { console.error("H_contract missing in taler-payment-succeeded"); return; } - console.log("got taler-payment-succeeded"); + logVerbose && console.log("got taler-payment-succeeded"); const walletMsg = { type: "payment-succeeded", detail: { diff --git a/lib/wallet/db.ts b/lib/wallet/db.ts index 9133330a2..a78abc26a 100644 --- a/lib/wallet/db.ts +++ b/lib/wallet/db.ts @@ -15,6 +15,7 @@ */ "use strict"; +import {IExchangeInfo} from "./types"; /** * Declarations and helpers for @@ -27,6 +28,7 @@ const DB_NAME = "taler"; const DB_VERSION = 10; + /** * Return a promise that resolves * to the taler wallet db. diff --git a/lib/wallet/query.ts b/lib/wallet/query.ts index fa78fe640..ce0308ed3 100644 --- a/lib/wallet/query.ts +++ b/lib/wallet/query.ts @@ -24,6 +24,16 @@ "use strict"; +export class Store { + name: string; + validator?: (v: T) => T; + + constructor(name: string, validator?: (v: T) => T) { + this.name = name; + this.validator = validator; + } +} + /** * Stream that can be filtered, reduced or joined * with indices. @@ -277,10 +287,10 @@ export class QueryRoot { this.db = db; } - iter(storeName: string, + iter(store: Store, {only = undefined, indexName = undefined} = {}): QueryStream { - this.stores.add(storeName); - return new IterQueryStream(this, storeName, { only, indexName }); + this.stores.add(store.name); + return new IterQueryStream(this, store.name, { only, indexName }); } /** @@ -288,11 +298,11 @@ export class QueryRoot { * Overrides if an existing object with the same key exists * in the store. */ - put(storeName: string, val: any): QueryRoot { + put(store: Store, val: T): QueryRoot { let doPut = (tx: IDBTransaction) => { - tx.objectStore(storeName).put(val); + tx.objectStore(store.name).put(val); }; - this.addWork(doPut, storeName, true); + this.addWork(doPut, store.name, true); return this; } @@ -302,13 +312,13 @@ export class QueryRoot { * Fails if the object's key is already present * in the object store. */ - putAll(storeName: string, iterable: any[]): QueryRoot { + putAll(store: Store, iterable: T[]): QueryRoot { const doPutAll = (tx: IDBTransaction) => { for (let obj of iterable) { - tx.objectStore(storeName).put(obj); + tx.objectStore(store.name).put(obj); } }; - this.addWork(doPutAll, storeName, true); + this.addWork(doPutAll, store.name, true); return this; } @@ -317,18 +327,18 @@ export class QueryRoot { * Fails if the object's key is already present * in the object store. */ - add(storeName: string, val: any): QueryRoot { + add(store: Store, val: T): QueryRoot { const doAdd = (tx: IDBTransaction) => { - tx.objectStore(storeName).add(val); + tx.objectStore(store.name).add(val); }; - this.addWork(doAdd, storeName, true); + this.addWork(doAdd, store.name, true); return this; } /** * Get one object from a store by its key. */ - get(storeName: any, key: any): Promise { + get(store: Store, key: any): Promise { if (key === void 0) { throw Error("key must not be undefined"); } @@ -336,13 +346,13 @@ export class QueryRoot { const {resolve, promise} = openPromise(); const doGet = (tx: IDBTransaction) => { - const req = tx.objectStore(storeName).get(key); + const req = tx.objectStore(store.name).get(key); req.onsuccess = () => { resolve(req.result); }; }; - this.addWork(doGet, storeName, false); + this.addWork(doGet, store.name, false); return Promise.resolve() .then(() => this.finish()) .then(() => promise); diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts index 85fd8c6f0..6ab777801 100644 --- a/lib/wallet/wallet.ts +++ b/lib/wallet/wallet.ts @@ -30,7 +30,7 @@ import { WireInfo, RefreshSession, ReserveRecord, CoinPaySig } from "./types"; import {HttpResponse, RequestException} from "./http"; -import {QueryRoot} from "./query"; +import {QueryRoot, Store} from "./query"; import {Checkable} from "./checkable"; import {canonicalizeBaseUrl} from "./helpers"; import {ReserveCreationInfo, Amounts} from "./types"; @@ -305,6 +305,17 @@ function getWithdrawDenomList(amountAvailable: AmountJson, } +namespace Stores { + export let exchanges: Store = new Store("exchanges"); + export let transactions: Store = new Store("transactions"); + export let reserves: Store = new Store("reserves"); + export let coins: Store = new Store("coins"); + export let refresh: Store = new Store("refresh"); + export let history: Store = new Store("history"); + export let precoins: Store = new Store("precoins"); +} + + export class Wallet { private db: IDBDatabase; private http: HttpRequestLibrary; @@ -351,7 +362,7 @@ export class Wallet { console.log("updating exchanges"); this.q() - .iter("exchanges") + .iter(Stores.exchanges) .reduce((exchange: IExchangeInfo) => { this.updateExchangeFromUrl(exchange.baseUrl) .catch((e) => { @@ -368,28 +379,28 @@ export class Wallet { console.log("resuming pending operations from db"); this.q() - .iter("reserves") - .reduce((reserve: any) => { + .iter(Stores.reserves) + .reduce((reserve) => { console.log("resuming reserve", reserve.reserve_pub); this.processReserve(reserve); }); this.q() - .iter("precoins") - .reduce((preCoin: any) => { + .iter(Stores.precoins) + .reduce((preCoin) => { console.log("resuming precoin"); this.processPreCoin(preCoin); }); this.q() - .iter("refresh") + .iter(Stores.refresh) .reduce((r: RefreshSession) => { this.continueRefreshSession(r); }); // FIXME: optimize via index this.q() - .iter("coins") + .iter(Stores.coins) .reduce((c: Coin) => { if (c.dirty && !c.transactionPending) { this.refresh(c.coinPub); @@ -452,7 +463,7 @@ export class Wallet { console.log("Checking for merchant's exchange", JSON.stringify(info)); return [ this.q() - .iter("exchanges", {indexName: "pubKey", only: info.master_pub}) + .iter(Stores.exchanges, {indexName: "pubKey", only: info.master_pub}) .indexJoin("coins", "exchangeBaseUrl", (exchange) => exchange.baseUrl) @@ -536,7 +547,7 @@ export class Wallet { merchantSig: offer.merchant_sig, }; - let historyEntry = { + let historyEntry: HistoryRecord = { type: "pay", timestamp: (new Date).getTime(), subjectId: `contract-${offer.H_contract}`, @@ -545,13 +556,14 @@ export class Wallet { amount: offer.contract.amount, contractHash: offer.H_contract, fulfillmentUrl: offer.contract.fulfillment_url, - } + }, + level: HistoryLevel.User }; await this.q() - .put("transactions", t) - .put("history", historyEntry) - .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) + .put(Stores.transactions, t) + .put(Stores.history, historyEntry) + .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin)) .finish(); this.notifier.notify(); @@ -559,7 +571,7 @@ export class Wallet { async putHistory(historyEntry: HistoryRecord): Promise { - await this.q().put("history", historyEntry).finish(); + await this.q().put(Stores.history, historyEntry).finish(); this.notifier.notify(); } @@ -571,7 +583,7 @@ export class Wallet { async confirmPay(offer: Offer): Promise { console.log("executing confirmPay"); - let transaction = await this.q().get("transactions", offer.H_contract); + let transaction = await this.q().get(Stores.transactions, offer.H_contract); if (transaction) { // Already payed ... @@ -604,7 +616,7 @@ export class Wallet { */ async checkPay(offer: Offer): Promise { // First check if we already payed for it. - let transaction = await this.q().get("transactions", offer.H_contract); + let transaction = await this.q().get(Stores.transactions, offer.H_contract); if (transaction) { return {isPayed: true}; } @@ -629,7 +641,7 @@ export class Wallet { * with the given hash. */ async executePayment(H_contract: string): Promise { - let t = await this.q().get("transactions", H_contract); + let t = await this.q().get(Stores.transactions, H_contract); if (!t) { return { success: false, @@ -661,7 +673,7 @@ export class Wallet { let n = await this.depleteReserve(reserve, exchange); if (n != 0) { - let depleted = { + let depleted: HistoryRecord = { type: "depleted-reserve", subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, timestamp: (new Date).getTime(), @@ -670,9 +682,10 @@ export class Wallet { reservePub: reserveRecord.reserve_pub, requestedAmount: reserveRecord.requested_amount, currentAmount: reserveRecord.current_amount, - } + }, + level: HistoryLevel.User }; - await this.q().put("history", depleted).finish(); + await this.q().put(Stores.history, depleted).finish(); } } catch (e) { // random, exponential backoff truncated at 3 minutes @@ -736,8 +749,8 @@ export class Wallet { }; await this.q() - .put("reserves", reserveRecord) - .put("history", historyEntry) + .put(Stores.reserves, reserveRecord) + .put(Stores.history, historyEntry) .finish(); let r: CreateReserveResponse = { @@ -760,14 +773,14 @@ export class Wallet { async confirmReserve(req: ConfirmReserveRequest): Promise { const now = (new Date).getTime(); let reserve: ReserveRecord|undefined = await ( - this.q().get("reserves", + this.q().get(Stores.reserves, req.reservePub)); if (!reserve) { console.error("Unable to confirm reserve, not found in DB"); return; } console.log("reserve confirmed"); - const historyEntry = { + const historyEntry: HistoryRecord = { type: "confirm-reserve", timestamp: now, subjectId: `reserve-progress-${reserve.reserve_pub}`, @@ -775,12 +788,13 @@ export class Wallet { exchangeBaseUrl: reserve.exchange_base_url, reservePub: req.reservePub, requestedAmount: reserve.requested_amount, - } + }, + level: HistoryLevel.User, }; reserve.confirmed = true; await this.q() - .put("reserves", reserve) - .put("history", historyEntry) + .put(Stores.reserves, reserve) + .put(Stores.history, historyEntry) .finish(); this.processReserve(reserve); @@ -788,7 +802,8 @@ export class Wallet { private async withdrawExecute(pc: PreCoin): Promise { - let reserve = await this.q().get("reserves", pc.reservePub); + let reserve = await this.q().get(Stores.reserves, + pc.reservePub); if (!reserve) { throw Error("db inconsistent"); @@ -837,8 +852,8 @@ export class Wallet { }; await this.q() .delete("precoins", coin.coinPub) - .add("coins", coin) - .add("history", historyEntry) + .add(Stores.coins, coin) + .add(Stores.history, historyEntry) .finish(); this.notifier.notify(); } @@ -853,7 +868,7 @@ export class Wallet { let preCoin = await this.cryptoApi .createPreCoin(denom, reserve); await this.q() - .put("precoins", preCoin) + .put(Stores.precoins, preCoin) .finish(); await this.processPreCoin(preCoin); } @@ -881,7 +896,7 @@ export class Wallet { private async updateReserve(reservePub: string, exchange: IExchangeInfo): Promise { let reserve = await this.q() - .get("reserves", reservePub); + .get(Stores.reserves, reservePub); if (!reserve) { throw Error("reserve not in db"); } @@ -910,7 +925,7 @@ export class Wallet { } }; await this.q() - .put("reserves", reserve) + .put(Stores.reserves, reserve) .finish(); return reserve; } @@ -982,7 +997,7 @@ export class Wallet { private async suspendCoins(exchangeInfo: IExchangeInfo): Promise { let suspendedCoins = await ( this.q() - .iter("coins", + .iter(Stores.coins, {indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl}) .reduce((coin: Coin, suspendedCoins: Coin[]) => { if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { @@ -995,7 +1010,7 @@ export class Wallet { suspendedCoins.map((c) => { console.log("suspending coin", c); c.suspended = true; - q.put("coins", c); + q.put(Stores.coins, c); }); await q.finish(); } @@ -1008,7 +1023,7 @@ export class Wallet { throw Error("invalid update time"); } - let r = await this.q().get("exchanges", baseUrl); + let r = await this.q().get(Stores.exchanges, baseUrl); let exchangeInfo: IExchangeInfo; @@ -1035,7 +1050,7 @@ export class Wallet { await this.suspendCoins(updatedExchangeInfo); await this.q() - .put("exchanges", updatedExchangeInfo) + .put(Stores.exchanges, updatedExchangeInfo) .finish(); return updatedExchangeInfo; @@ -1120,7 +1135,7 @@ export class Wallet { let byCurrency = await ( this.q() - .iter("coins") + .iter(Stores.coins) .reduce(collectBalances, {})); return {balances: byCurrency}; @@ -1131,13 +1146,13 @@ export class Wallet { // FIXME: this is not running in a transaction. - let coin = await this.q().get("coins", oldCoinPub); + let coin = await this.q().get(Stores.coins, oldCoinPub); if (!coin) { throw Error("coin not found"); } - let exchange = await this.q().get("exchanges", + let exchange = await this.q().get(Stores.exchanges, coin.exchangeBaseUrl); if (!exchange) { throw Error("db inconsistent"); @@ -1177,8 +1192,8 @@ export class Wallet { // FIXME: we should check whether the amount still matches! await this.q() - .put("refresh", refreshSession) - .put("coins", coin) + .put(Stores.refresh, refreshSession) + .put(Stores.coins, coin) .finish(); return refreshSession; @@ -1187,12 +1202,12 @@ export class Wallet { async refresh(oldCoinPub: string): Promise { let refreshSession: RefreshSession|undefined; - let oldSession = await this.q().get("refresh", oldCoinPub); + let oldSession = await this.q().get(Stores.refresh, oldCoinPub); if (oldSession) { refreshSession = oldSession; } else { - refreshSession = await this.q().get("refresh", - oldCoinPub); + refreshSession = await this.q().get(Stores.refresh, + oldCoinPub); } if (!refreshSession) { // refreshing not necessary @@ -1208,7 +1223,7 @@ export class Wallet { if (typeof refreshSession.norevealIndex !== "number") { let coinPub = refreshSession.meltCoinPub; await this.refreshMelt(refreshSession); - let r = await this.q().get("refresh", coinPub); + let r = await this.q().get(Stores.refresh, coinPub); if (!r) { throw Error("refresh session does not exist anymore"); } @@ -1225,7 +1240,7 @@ export class Wallet { return; } - let coin = await this.q().get("coins", refreshSession.meltCoinPub); + let coin = await this.q().get(Stores.coins, refreshSession.meltCoinPub); if (!coin) { console.error("can't melt coin, it does not exist"); return; @@ -1271,7 +1286,7 @@ export class Wallet { refreshSession.norevealIndex = norevealIndex; - await this.q().put("refresh", refreshSession).finish(); + await this.q().put(Stores.refresh, refreshSession).finish(); } @@ -1307,7 +1322,7 @@ export class Wallet { console.log("/refresh/reveal did not contain ev_sigs"); } - let exchange = await this.q().get("exchanges", + let exchange = await this.q().get(Stores.exchanges, refreshSession.exchangeBaseUrl); if (!exchange) { console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`); @@ -1343,8 +1358,8 @@ export class Wallet { refreshSession.finished = true; await this.q() - .putAll("coins", coins) - .put("refresh", refreshSession) + .putAll(Stores.coins, coins) + .put(Stores.refresh, refreshSession) .finish(); } @@ -1360,7 +1375,7 @@ export class Wallet { let history = await ( this.q() - .iter("history", {indexName: "timestamp"}) + .iter(Stores.history, {indexName: "timestamp"}) .reduce(collect, [])); return {history}; @@ -1368,28 +1383,28 @@ export class Wallet { async getExchanges(): Promise { return this.q() - .iter("exchanges") + .iter(Stores.exchanges) .flatMap((e) => [e]) .toArray(); } async getReserves(exchangeBaseUrl: string): Promise { return this.q() - .iter("reserves") + .iter(Stores.reserves) .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl) .toArray(); } async getCoins(exchangeBaseUrl: string): Promise { return this.q() - .iter("coins") + .iter(Stores.coins) .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl) .toArray(); } async getPreCoins(exchangeBaseUrl: string): Promise { return this.q() - .iter("precoins") + .iter(Stores.precoins) .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl) .toArray(); } @@ -1431,19 +1446,19 @@ export class Wallet { async paymentSucceeded(contractHash: string): Promise { const doPaymentSucceeded = async() => { - let t = await this.q().get("transactions", contractHash); + let t = await this.q().get(Stores.transactions, contractHash); if (!t) { console.error("contract not found"); return; } for (let pc of t.payReq.coins) { - let c = await this.q().get("coins", pc.coin_pub); + let c = await this.q().get(Stores.coins, pc.coin_pub); if (!c) { console.error("coin not found"); return; } c.transactionPending = false; - await this.q().put("coins", c).finish(); + await this.q().put(Stores.coins, c).finish(); } for (let c of t.payReq.coins) { this.refresh(c.coin_pub); @@ -1452,4 +1467,4 @@ export class Wallet { doPaymentSucceeded(); return; } -} \ No newline at end of file +}