summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-11-20 19:48:43 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-11-20 19:48:43 +0100
commit553da649902f71d5ca34c9a6289ab6b1ef0ba7cb (patch)
tree857c4eb2c39e4a92e71c8a623d3188e6dbbbd1e9
parentfaedf697626dd37f3ac74ad4cac1ec378598bbf3 (diff)
downloadwallet-core-553da649902f71d5ca34c9a6289ab6b1ef0ba7cb.tar.gz
wallet-core-553da649902f71d5ca34c9a6289ab6b1ef0ba7cb.tar.bz2
wallet-core-553da649902f71d5ca34c9a6289ab6b1ef0ba7cb.zip
WIP: simplify DB queries and error handling
-rw-r--r--package.json2
-rw-r--r--packages/idb-bridge/.vscode/settings.json3
-rw-r--r--src/dbTypes.ts90
-rw-r--r--src/headless/clk.ts64
-rw-r--r--src/headless/taler-wallet-cli.ts108
-rw-r--r--src/helpers.ts14
-rw-r--r--src/logging.ts351
-rw-r--r--src/query.ts1112
-rw-r--r--src/wallet.ts1400
-rw-r--r--src/walletTypes.ts58
-rw-r--r--src/webex/messages.ts16
-rw-r--r--src/webex/pages/error.html18
-rw-r--r--src/webex/pages/error.tsx129
-rw-r--r--src/webex/pages/logs.html27
-rw-r--r--src/webex/pages/logs.tsx86
-rw-r--r--src/webex/renderHtml.tsx6
-rw-r--r--src/webex/wxApi.ts32
-rw-r--r--src/webex/wxBackend.ts42
-rw-r--r--tsconfig.json3
-rw-r--r--webpack.config.js2
-rw-r--r--yarn.lock8
21 files changed, 1131 insertions, 2440 deletions
diff --git a/package.json b/package.json
index 8e7a5b355..d8511c409 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
"@types/urijs": "^1.19.3",
"axios": "^0.19.0",
"commander": "^3.0.1",
- "idb-bridge": "^0.0.10",
+ "idb-bridge": "^0.0.11",
"qrcode-generator": "^1.4.3",
"source-map-support": "^0.5.12",
"urijs": "^1.18.10"
diff --git a/packages/idb-bridge/.vscode/settings.json b/packages/idb-bridge/.vscode/settings.json
index ff30c4464..ec71f9aae 100644
--- a/packages/idb-bridge/.vscode/settings.json
+++ b/packages/idb-bridge/.vscode/settings.json
@@ -1,3 +1,4 @@
{
- "editor.tabSize": 2
+ "editor.tabSize": 2,
+ "typescript.tsdk": "node_modules/typescript/lib"
} \ No newline at end of file
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 28893b8eb..0d54069ec 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -36,6 +36,7 @@ import {
} from "./talerTypes";
import { Index, Store } from "./query";
+import { Timestamp, OperationError } from "./walletTypes";
/**
* Current database version, should be incremented
@@ -310,13 +311,10 @@ export class DenominationRecord {
}
/**
- * Exchange record as stored in the wallet's database.
+ * Details about the exchange that we only know after
+ * querying /keys and /wire.
*/
-export interface ExchangeRecord {
- /**
- * Base url of the exchange.
- */
- baseUrl: string;
+export interface ExchangeDetails {
/**
* Master public key of the exchange.
*/
@@ -332,21 +330,59 @@ export interface ExchangeRecord {
currency: string;
/**
+ * Last observed protocol version.
+ */
+ protocolVersion: string;
+
+ /**
* Timestamp for last update.
*/
- lastUpdateTime: number;
+ lastUpdateTime: Timestamp;
+}
+
+export enum ExchangeUpdateStatus {
+ NONE = "none",
+ FETCH_KEYS = "fetch_keys",
+ FETCH_WIRE = "fetch_wire",
+}
+
+export interface ExchangeBankAccount {
+ url: string;
+}
+export interface ExchangeWireInfo {
+ feesForType: { [wireMethod: string]: WireFee[] };
+ accounts: ExchangeBankAccount[];
+}
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+export interface ExchangeRecord {
/**
- * When did we actually use this exchange last (in milliseconds). If we
- * never used the exchange for anything but just updated its info, this is
- * set to 0. (Currently only updated when reserves are created.)
+ * Base url of the exchange.
*/
- lastUsedTime: number;
+ baseUrl: string;
/**
- * Last observed protocol version.
+ * Details, once known.
*/
- protocolVersion?: string;
+ details: ExchangeDetails | undefined;
+
+ /**
+ * Mapping from wire method type to the wire fee.
+ */
+ wireInfo: ExchangeWireInfo | undefined;
+
+ /**
+ * Time when the update to the exchange has been started or
+ * undefined if no update is in progress.
+ */
+ updateStarted: Timestamp | undefined;
+
+ updateStatus: ExchangeUpdateStatus;
+
+ lastError?: OperationError;
}
/**
@@ -555,21 +591,6 @@ export class ProposalDownloadRecord {
}
/**
- * Wire fees for an exchange.
- */
-export interface ExchangeWireFeesRecord {
- /**
- * Base URL of the exchange.
- */
- exchangeBaseUrl: string;
-
- /**
- * Mapping from wire method type to the wire fee.
- */
- feesForType: { [wireMethod: string]: WireFee[] };
-}
-
-/**
* Status of a tip we got from a merchant.
*/
export interface TipRecord {
@@ -931,12 +952,6 @@ export namespace Stores {
constructor() {
super("exchanges", { keyPath: "baseUrl" });
}
-
- pubKeyIndex = new Index<string, ExchangeRecord>(
- this,
- "pubKeyIndex",
- "masterPublicKey",
- );
}
class CoinsStore extends Store<CoinRecord> {
@@ -1034,12 +1049,6 @@ export namespace Stores {
}
}
- class ExchangeWireFeesStore extends Store<ExchangeWireFeesRecord> {
- constructor() {
- super("exchangeWireFees", { keyPath: "exchangeBaseUrl" });
- }
- }
-
class ReservesStore extends Store<ReserveRecord> {
constructor() {
super("reserves", { keyPath: "reserve_pub" });
@@ -1094,7 +1103,6 @@ export namespace Stores {
export const config = new ConfigStore();
export const currencies = new CurrenciesStore();
export const denominations = new DenominationsStore();
- export const exchangeWireFees = new ExchangeWireFeesStore();
export const exchanges = new ExchangeStore();
export const precoins = new Store<PreCoinRecord>("precoins", {
keyPath: "coinPub",
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
index 642a1bef3..f66d609e8 100644
--- a/src/headless/clk.ts
+++ b/src/headless/clk.ts
@@ -20,7 +20,6 @@
import process = require("process");
import path = require("path");
import readline = require("readline");
-import { symlinkSync } from "fs";
class Converter<T> {}
@@ -54,6 +53,7 @@ interface ArgumentDef {
name: string;
conv: Converter<any>;
args: ArgumentArgs<any>;
+ required: boolean;
}
interface SubcommandDef {
@@ -181,7 +181,7 @@ export class CommandGroup<GN extends keyof any, TG> {
return this as any;
}
- argument<N extends keyof any, V>(
+ requiredArgument<N extends keyof any, V>(
name: N,
conv: Converter<V>,
args: ArgumentArgs<V> = {},
@@ -190,6 +190,22 @@ export class CommandGroup<GN extends keyof any, TG> {
args: args,
conv: conv,
name: name as string,
+ required: true,
+ };
+ this.arguments.push(argDef);
+ return this as any;
+ }
+
+ maybeArgument<N extends keyof any, V>(
+ name: N,
+ conv: Converter<V>,
+ args: ArgumentArgs<V> = {},
+ ): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> {
+ const argDef: ArgumentDef = {
+ args: args,
+ conv: conv,
+ name: name as string,
+ required: false,
};
this.arguments.push(argDef);
return this as any;
@@ -401,10 +417,25 @@ export class CommandGroup<GN extends keyof any, TG> {
process.exit(-1);
throw Error("not reached");
}
+ myArgs[d.name] = unparsedArgs[i];
posArgIndex++;
}
}
+ for (let i = posArgIndex; i < this.arguments.length; i++) {
+ const d = this.arguments[i];
+ const n = this.name ?? progname;
+ if (d.required) {
+ if (d.args.default !== undefined) {
+ myArgs[d.name] = d.args.default;
+ } else {
+ console.error(`error: missing positional argument '${d.name}' for ${n}`);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ }
+ }
+
for (let option of this.options) {
if (option.isFlag == false && option.required == true) {
if (!foundOptions[option.name]) {
@@ -433,9 +464,7 @@ export class CommandGroup<GN extends keyof any, TG> {
unparsedArgs.slice(i + 1),
parsedArgs,
);
- }
-
- if (this.myAction) {
+ } else if (this.myAction) {
this.myAction(parsedArgs);
} else {
this.printHelp(progname, parents);
@@ -513,18 +542,35 @@ export class Program<PN extends keyof any, T> {
}
/**
- * Add a positional argument to the program.
+ * Add a required positional argument to the program.
*/
- argument<N extends keyof any, V>(
+ requiredArgument<N extends keyof any, V>(
name: N,
conv: Converter<V>,
args: ArgumentArgs<V> = {},
): Program<N, T & SubRecord<PN, N, V>> {
- this.mainCommand.argument(name, conv, args);
+ this.mainCommand.requiredArgument(name, conv, args);
+ return this as any;
+ }
+
+ /**
+ * Add an optional argument to the program.
+ */
+ maybeArgument<N extends keyof any, V>(
+ name: N,
+ conv: Converter<V>,
+ args: ArgumentArgs<V> = {},
+ ): Program<N, T & SubRecord<PN, N, V | undefined>> {
+ this.mainCommand.maybeArgument(name, conv, args);
return this as any;
}
}
+export type GetArgType<T> =
+ T extends Program<any, infer AT> ? AT :
+ T extends CommandGroup<any, infer AT> ? AT :
+ any;
+
export function program<PN extends keyof any>(
argKey: PN,
args: ProgramArgs = {},
@@ -532,6 +578,8 @@ export function program<PN extends keyof any>(
return new Program(argKey as string, args);
}
+
+
export function prompt(question: string): Promise<string> {
const stdinReadline = readline.createInterface({
input: process.stdin,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 41f68319a..06235d0b4 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -97,6 +97,28 @@ const walletCli = clk
help: "Enable verbose output.",
});
+type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
+
+async function withWallet<T>(
+ walletCliArgs: WalletCliArgsType,
+ f: (w: Wallet) => Promise<T>,
+): Promise<T> {
+ applyVerbose(walletCliArgs.wallet.verbose);
+ const wallet = await getDefaultNodeWallet({
+ persistentStoragePath: walletDbPath,
+ });
+ try {
+ await wallet.fillDefaults();
+ const ret = await f(wallet);
+ return ret;
+ } catch (e) {
+ console.error("caught exception:", e);
+ process.exit(1);
+ } finally {
+ wallet.stop();
+ }
+}
+
walletCli
.subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
.requiredOption("amount", ["-a", "--amount"], clk.STRING)
@@ -135,15 +157,11 @@ walletCli
walletCli
.subcommand("", "balance", { help: "Show wallet balance." })
.action(async args => {
- applyVerbose(args.wallet.verbose);
console.log("balance command called");
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
+ withWallet(args, async (wallet) => {
+ const balance = await wallet.getBalances();
+ console.log(JSON.stringify(balance, undefined, 2));
});
- console.log("got wallet");
- const balance = await wallet.getBalances();
- console.log(JSON.stringify(balance, undefined, 2));
- process.exit(0);
});
walletCli
@@ -153,29 +171,19 @@ walletCli
.requiredOption("limit", ["--limit"], clk.STRING)
.requiredOption("contEvt", ["--continue-with"], clk.STRING)
.action(async args => {
- applyVerbose(args.wallet.verbose);
- console.log("history command called");
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
+ withWallet(args, async (wallet) => {
+ const history = await wallet.getHistory();
+ console.log(JSON.stringify(history, undefined, 2));
});
- console.log("got wallet");
- const history = await wallet.getHistory();
- console.log(JSON.stringify(history, undefined, 2));
- process.exit(0);
});
walletCli
.subcommand("", "pending", { help: "Show pending operations." })
.action(async args => {
- applyVerbose(args.wallet.verbose);
- console.log("history command called");
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
+ withWallet(args, async (wallet) => {
+ const pending = await wallet.getPendingOperations();
+ console.log(JSON.stringify(pending, undefined, 2));
});
- console.log("got wallet");
- const pending = await wallet.getPendingOperations();
- console.log(JSON.stringify(pending, undefined, 2));
- process.exit(0);
});
async function asyncSleep(milliSeconds: number): Promise<void> {
@@ -185,6 +193,16 @@ async function asyncSleep(milliSeconds: number): Promise<void> {
}
walletCli
+ .subcommand("runPendingOpt", "run-pending", {
+ help: "Run pending operations."
+ })
+ .action(async (args) => {
+ withWallet(args, async (wallet) => {
+ await wallet.processPending();
+ });
+ });
+
+walletCli
.subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:1",
@@ -279,7 +297,7 @@ walletCli
walletCli
.subcommand("withdrawUriCmd", "withdraw-uri")
- .argument("withdrawUri", clk.STRING)
+ .requiredArgument("withdrawUri", clk.STRING)
.action(async args => {
applyVerbose(args.wallet.verbose);
const cmdArgs = args.withdrawUriCmd;
@@ -318,7 +336,7 @@ walletCli
walletCli
.subcommand("tipUriCmd", "tip-uri")
- .argument("uri", clk.STRING)
+ .requiredArgument("uri", clk.STRING)
.action(async args => {
applyVerbose(args.wallet.verbose);
const tipUri = args.tipUriCmd.uri;
@@ -334,7 +352,7 @@ walletCli
walletCli
.subcommand("refundUriCmd", "refund-uri")
- .argument("uri", clk.STRING)
+ .requiredArgument("uri", clk.STRING)
.action(async args => {
applyVerbose(args.wallet.verbose);
const refundUri = args.refundUriCmd.uri;
@@ -346,20 +364,38 @@ walletCli
wallet.stop();
});
-const exchangesCli = walletCli
- .subcommand("exchangesCmd", "exchanges", {
- help: "Manage exchanges."
- });
-
-exchangesCli.subcommand("exchangesListCmd", "list", {
- help: "List known exchanges."
+const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
+ help: "Manage exchanges.",
});
-exchangesCli.subcommand("exchangesListCmd", "update");
+exchangesCli
+ .subcommand("exchangesListCmd", "list", {
+ help: "List known exchanges.",
+ })
+ .action(async args => {
+ console.log("Listing exchanges ...");
+ withWallet(args, async (wallet) => {
+ const exchanges = await wallet.getExchanges();
+ console.log("exchanges", exchanges);
+ });
+ });
+
+exchangesCli
+ .subcommand("exchangesUpdateCmd", "update", {
+ help: "Update or add an exchange by base URL.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .action(async args => {
+ withWallet(args, async (wallet) => {
+ const res = await wallet.updateExchangeFromUrl(args.exchangesUpdateCmd.url);
+ });
+ });
walletCli
.subcommand("payUriCmd", "pay-uri")
- .argument("url", clk.STRING)
+ .requiredArgument("url", clk.STRING)
.flag("autoYes", ["-y", "--yes"])
.action(async args => {
applyVerbose(args.wallet.verbose);
@@ -374,7 +410,7 @@ walletCli
});
const testCli = walletCli.subcommand("testingArgs", "testing", {
- help: "Subcommands for testing GNU Taler deployments."
+ help: "Subcommands for testing GNU Taler deployments.",
});
testCli
diff --git a/src/helpers.ts b/src/helpers.ts
index 7cd176498..a063db169 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -25,6 +25,7 @@ import { AmountJson } from "./amounts";
import * as Amounts from "./amounts";
import URI = require("urijs");
+import { Timestamp } from "./walletTypes";
/**
* Show an amount in a form suitable for the user.
@@ -126,6 +127,19 @@ export function getTalerStampSec(stamp: string): number | null {
}
/**
+ * Extract a timestamp from a Taler timestamp string.
+ */
+export function extractTalerStamp(stamp: string): Timestamp | undefined {
+ const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
+ if (!m || !m[1]) {
+ return undefined;
+ }
+ return {
+ t_ms: parseInt(m[1], 10) * 1000,
+ };
+}
+
+/**
* Check if a timestamp is in the right format.
*/
export function timestampCheck(stamp: string): boolean {
diff --git a/src/logging.ts b/src/logging.ts
deleted file mode 100644
index 4e7b60b93..000000000
--- a/src/logging.ts
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 Inria
-
- 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/>
- */
-
-/**
- * Configurable logging. Allows to log persistently to a database.
- */
-
-import {
- QueryRoot,
- Store,
-} from "./query";
-import { openPromise } from "./promiseUtils";
-
-/**
- * Supported log levels.
- */
-export type Level = "error" | "debug" | "info" | "warn";
-
-// Right now, our debug/info/warn/debug loggers just use the console based
-// loggers. This might change in the future.
-
-function makeInfo() {
- return console.info.bind(console, "%o");
-}
-
-function makeWarn() {
- return console.warn.bind(console, "%o");
-}
-
-function makeError() {
- return console.error.bind(console, "%o");
-}
-
-function makeDebug() {
- return console.log.bind(console, "%o");
-}
-
-/**
- * Log a message using the configurable logger.
- */
-export async function log(msg: string, level: Level = "info"): Promise<void> {
- const ci = getCallInfo(2);
- return record(level, msg, undefined, ci.file, ci.line, ci.column);
-}
-
-function getCallInfo(level: number) {
- // see https://github.com/v8/v8/wiki/Stack-Trace-API
- const stack = Error().stack;
- if (!stack) {
- return unknownFrame;
- }
- const lines = stack.split("\n");
- return parseStackLine(lines[level + 1]);
-}
-
-interface Frame {
- column?: number;
- file?: string;
- line?: number;
- method?: string;
-}
-
-const unknownFrame: Frame = {
- column: 0,
- file: "(unknown)",
- line: 0,
- method: "(unknown)",
-};
-
-/**
- * Adapted from https://github.com/errwischt/stacktrace-parser.
- */
-function parseStackLine(stackLine: string): Frame {
- // tslint:disable-next-line:max-line-length
- const chrome = /^\s*at (?:(?:(?:Anonymous function)?|((?:\[object object\])?\S+(?: \[as \S+\])?)) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
- const gecko = /^(?:\s*([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;
- const node = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
- let parts;
-
- parts = gecko.exec(stackLine);
- if (parts) {
- const f: Frame = {
- column: parts[5] ? +parts[5] : undefined,
- file: parts[3],
- line: +parts[4],
- method: parts[1] || "(unknown)",
- };
- return f;
- }
-
- parts = chrome.exec(stackLine);
- if (parts) {
- const f: Frame = {
- column: parts[4] ? +parts[4] : undefined,
- file: parts[2],
- line: +parts[3],
- method: parts[1] || "(unknown)",
- };
- return f;
- }
-
- parts = node.exec(stackLine);
- if (parts) {
- const f: Frame = {
- column: parts[4] ? +parts[4] : undefined,
- file: parts[2],
- line: +parts[3],
- method: parts[1] || "(unknown)",
- };
- return f;
- }
-
- return unknownFrame;
-}
-
-
-let db: IDBDatabase|undefined;
-
-/**
- * A structured log entry as stored in the database.
- */
-export interface LogEntry {
- /**
- * Soure code column where the error occured.
- */
- col?: number;
- /**
- * Additional detail for the log statement.
- */
- detail?: string;
- /**
- * Id of the log entry, used as primary
- * key for the database.
- */
- id?: number;
- /**
- * Log level, see [[Level}}.
- */
- level: string;
- /**
- * Line where the log was created from.
- */
- line?: number;
- /**
- * The actual log message.
- */
- msg: string;
- /**
- * The source file where the log enctry
- * was created from.
- */
- source?: string;
- /**
- * Time when the log entry was created.
- */
- timestamp: number;
-}
-
-/**
- * Get all logs. Only use for debugging, since this returns all logs ever made
- * at once without pagination.
- */
-export async function getLogs(): Promise<LogEntry[]> {
- if (!db) {
- db = await openLoggingDb();
- }
- return await new QueryRoot(db).iter(logsStore).toArray();
-}
-
-/**
- * The barrier ensures that only one DB write is scheduled against the log db
- * at the same time, so that the DB can stay responsive. This is a bit of a
- * design problem with IndexedDB, it doesn't guarantee fairness.
- */
-let barrier: any;
-
-/**
- * Record an exeption in the log.
- */
-export async function recordException(msg: string, e: any): Promise<void> {
- let stack: string|undefined;
- let frame: Frame|undefined;
- try {
- stack = e.stack;
- if (stack) {
- const lines = stack.split("\n");
- frame = parseStackLine(lines[1]);
- }
- } catch (e) {
- // ignore
- }
- if (!frame) {
- frame = unknownFrame;
- }
- return record("error", e.toString(), stack, frame.file, frame.line, frame.column);
-}
-
-
-/**
- * Cache for reports. Also used when something is so broken that we can't even
- * access the database.
- */
-const reportCache: { [reportId: string]: any } = {};
-
-
-/**
- * Get a UUID that does not use cryptographically secure randomness.
- * Formatted as RFC4122 version 4 UUID.
- */
-function getInsecureUuid() {
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c: string) => {
- const r = Math.random() * 16 | 0;
- const v = c === "x" ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
-}
-
-
-/**
- * Store a report and return a unique identifier to retrieve it later.
- */
-export async function storeReport(report: any): Promise<string> {
- const uid = getInsecureUuid();
- reportCache[uid] = report;
- return uid;
-}
-
-
-/**
- * Retrieve a report by its unique identifier.
- */
-export async function getReport(reportUid: string): Promise<any> {
- return reportCache[reportUid];
-}
-
-
-/**
- * Record a log entry in the database.
- */
-export async function record(level: Level,
- msg: string,
- detail?: string,
- source?: string,
- line?: number,
- col?: number): Promise<void> {
- if (typeof indexedDB === "undefined") {
- console.log("can't access DB for logging in this context");
- console.log("log was", { level, msg, detail, source, line, col });
- return;
- }
-
- let myBarrier: any;
-
- if (barrier) {
- const p = barrier.promise;
- myBarrier = barrier = openPromise();
- await p;
- } else {
- myBarrier = barrier = openPromise();
- }
-
- try {
- if (!db) {
- db = await openLoggingDb();
- }
-
- const count = await new QueryRoot(db).count(logsStore);
-
- if (count > 1000) {
- await new QueryRoot(db).deleteIf(logsStore, (e, i) => (i < 200));
- }
-
- const entry: LogEntry = {
- col,
- detail,
- level,
- line,
- msg,
- source,
- timestamp: new Date().getTime(),
- };
- await new QueryRoot(db).put(logsStore, entry);
- } finally {
- await Promise.resolve().then(() => myBarrier.resolve());
- }
-}
-
-const loggingDbVersion = 2;
-
-const logsStore: Store<LogEntry> = new Store<LogEntry>("logs");
-
-/**
- * Get a handle to the IndexedDB used to store
- * logs.
- */
-export function openLoggingDb(): Promise<IDBDatabase> {
- return new Promise<IDBDatabase>((resolve, reject) => {
- const req = indexedDB.open("taler-logging", loggingDbVersion);
- req.onerror = (e) => {
- reject(e);
- };
- req.onsuccess = (e) => {
- resolve(req.result);
- };
- req.onupgradeneeded = (e) => {
- const resDb = req.result;
- if (e.oldVersion !== 0) {
- try {
- resDb.deleteObjectStore("logs");
- } catch (e) {
- console.error(e);
- }
- }
- resDb.createObjectStore("logs", { keyPath: "id", autoIncrement: true });
- resDb.createObjectStore("reports", { keyPath: "uid", autoIncrement: false });
- };
- });
-}
-
-/**
- * Log a message at severity info.
- */
-export const info = makeInfo();
-
-/**
- * Log a message at severity debug.
- */
-export const debug = makeDebug();
-
-/**
- * Log a message at severity warn.
- */
-export const warn = makeWarn();
-
-/**
- * Log a message at severity error.
- */
-export const error = makeError();
diff --git a/src/query.ts b/src/query.ts
index 7c9390467..7d03cfea8 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -1,3 +1,5 @@
+import { openPromise } from "./promiseUtils";
+
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
@@ -20,9 +22,6 @@
* @author Florian Dold
*/
- import { openPromise } from "./promiseUtils";
-import { join } from "path";
-
/**
* Result of an inner join.
*/
@@ -63,928 +62,335 @@ export interface IndexOptions {
multiEntry?: boolean;
}
-/**
- * Definition of an index.
- */
-export class Index<S extends IDBValidKey, T> {
- /**
- * Name of the store that this index is associated with.
- */
- storeName: string;
-
- /**
- * Options to use for the index.
- */
- options: IndexOptions;
-
- constructor(
- s: Store<T>,
- public indexName: string,
- public keyPath: string | string[],
- options?: IndexOptions,
- ) {
- const defaultOptions = {
- multiEntry: false,
+function requestToPromise(req: IDBRequest): Promise<any> {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ resolve(req.result);
};
- this.options = { ...defaultOptions, ...(options || {}) };
- this.storeName = s.name;
- }
-
- /**
- * We want to have the key type parameter in use somewhere,
- * because otherwise the compiler complains. In iterIndex the
- * key type is pretty useful.
- */
- protected _dummyKey: S | undefined;
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
}
-/**
- * Stream that can be filtered, reduced or joined
- * with indices.
- */
-export interface QueryStream<T> {
- /**
- * Join the current query with values from an index.
- * The left side of the join is extracted via a function from the stream's
- * result, the right side of the join is the key of the index.
- */
- indexJoin<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>>;
- /**
- * Join the current query with values from an index, and keep values in the
- * current stream that don't have a match. The left side of the join is
- * extracted via a function from the stream's result, the right side of the
- * join is the key of the index.
- */
- indexJoinLeft<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinLeftResult<T, S>>;
- /**
- * Join the current query with values from another object store.
- * The left side of the join is extracted via a function over the current query,
- * the right side of the join is the key of the object store.
- */
- keyJoin<S, I extends IDBValidKey>(
- store: Store<S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>>;
-
- /**
- * Only keep elements in the result stream for which the predicate returns
- * true.
- */
- filter(f: (x: T) => boolean): QueryStream<T>;
-
- /**
- * Fold the stream, resulting in a single value.
- */
- fold<S>(f: (v: T, acc: S) => S, start: S): Promise<S>;
-
- /**
- * Execute a function for every value of the stream, for the
- * side-effects of the function.
- */
- forEach(f: (v: T) => void): Promise<void>;
-
- /**
- * Map each element of the stream using a function, resulting in another
- * stream of a different type.
- */
- map<S>(f: (x: T) => S): QueryStream<S>;
-
- /**
- * Map each element of the stream to a potentially empty array, and collect
- * the result in a stream of the flattened arrays.
- */
- flatMap<S>(f: (x: T) => S[]): QueryStream<S>;
-
- /**
- * Collect the stream into an array and return a promise for it.
- */
- toArray(): Promise<T[]>;
-
- /**
- * Get the first value of the stream.
- */
- first(): QueryValue<T>;
-
- /**
- * Run the query without returning a result.
- * Useful for queries with side effects.
- */
- run(): Promise<void>;
+export function oneShotGet<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
}
-/**
- * Query result that consists of at most one value.
- */
-export interface QueryValue<T> {
- /**
- * Apply a function to a query value.
- */
- map<S>(f: (x: T) => S): QueryValue<S>;
- /**
- * Conditionally execute either of two queries based
- * on a property of this query value.
- *
- * Useful to properly implement complex queries within a transaction (as
- * opposed to just computing the conditional and then executing either
- * branch). This is necessary since IndexedDB does not allow long-lived
- * transactions.
- */
- cond<R>(
- f: (x: T) => boolean,
- onTrue: (r: QueryRoot) => R,
- onFalse: (r: QueryRoot) => R,
- ): Promise<void>;
+export function oneShotGetIndexed<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = db.transaction([index.storeName], "readonly");
+ const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
+ return requestToPromise(req);
}
-abstract class BaseQueryValue<T> implements QueryValue<T> {
- constructor(public root: QueryRoot) {}
-
- map<S>(f: (x: T) => S): QueryValue<S> {
- return new MapQueryValue<T, S>(this, f);
- }
+export function oneShotPut<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ value: T,
+ key?: any,
+): Promise<any> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
+}
- cond<R>(
- f: (x: T) => boolean,
- onTrue: (r: QueryRoot) => R,
- onFalse: (r: QueryRoot) => R,
- ): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- this.subscribeOne((v, tx) => {
- if (f(v)) {
- onTrue(new QueryRoot(this.root.db));
+function applyMutation<T>(req: IDBRequest, f: (x: T) => T | undefined): Promise<void> {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ const cursor = req.result;
+ if (cursor) {
+ const val = cursor.value();
+ const modVal = f(val);
+ if (modVal !== undefined && modVal !== null) {
+ const req2: IDBRequest = cursor.update(modVal);
+ req2.onerror = () => {
+ reject(req2.error);
+ };
+ req2.onsuccess = () => {
+ cursor.continue();
+ };
} else {
- onFalse(new QueryRoot(this.root.db));
+ cursor.continue();
}
- });
- resolve();
- });
- }
-
- abstract subscribeOne(f: SubscribeOneFn): void;
-}
-
-class FirstQueryValue<T> extends BaseQueryValue<T> {
- private gotValue = false;
- private s: QueryStreamBase<T>;
-
- constructor(stream: QueryStreamBase<T>) {
- super(stream.root);
- this.s = stream;
- }
-
- subscribeOne(f: SubscribeOneFn): void {
- this.s.subscribe((isDone, value, tx) => {
- if (this.gotValue) {
- return;
- }
- if (isDone) {
- f(undefined, tx);
} else {
- f(value, tx);
+ resolve();
}
- this.gotValue = true;
- });
- }
+ };
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
}
-class MapQueryValue<T, S> extends BaseQueryValue<S> {
- constructor(private v: BaseQueryValue<T>, private mapFn: (x: T) => S) {
- super(v.root);
- }
-
- subscribeOne(f: SubscribeOneFn): void {
- this.v.subscribeOne((v, tx) => this.mapFn(v));
- }
+export function oneShotMutate<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
}
-/**
- * Exception that should be thrown by client code to abort a transaction.
- */
-export const AbortTransaction = Symbol("abort_transaction");
-
-abstract class QueryStreamBase<T> implements QueryStream<T> {
- abstract subscribe(
- f: (isDone: boolean, value: any, tx: IDBTransaction) => void,
- ): void;
- constructor(public root: QueryRoot) {}
-
- first(): QueryValue<T> {
- return new FirstQueryValue(this);
- }
-
- flatMap<S>(f: (x: T) => S[]): QueryStream<S> {
- return new QueryStreamFlatMap<T, S>(this, f);
- }
+type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
- map<S>(f: (x: T) => S): QueryStream<S> {
- return new QueryStreamMap(this, f);
- }
-
- indexJoin<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>> {
- this.root.addStoreAccess(index.storeName, false);
- return new QueryStreamIndexJoin<T, S>(
- this,
- index.storeName,
- index.indexName,
- keyFn,
- );
- }
-
- indexJoinLeft<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinLeftResult<T, S>> {
- this.root.addStoreAccess(index.storeName, false);
- return new QueryStreamIndexJoinLeft<T, S>(
- this,
- index.storeName,
- index.indexName,
- keyFn,
- );
- }
-
- keyJoin<S, I extends IDBValidKey>(
- store: Store<S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>> {
- this.root.addStoreAccess(store.name, false);
- return new QueryStreamKeyJoin<T, S>(this, store.name, keyFn);
- }
-
- filter(f: (x: any) => boolean): QueryStream<T> {
- return new QueryStreamFilter(this, f);
- }
-
- toArray(): Promise<T[]> {
- const { resolve, promise } = openPromise<T[]>();
- const values: T[] = [];
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve(values);
- return;
- }
- values.push(value);
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
-
- fold<A>(f: (x: T, acc: A) => A, init: A): Promise<A> {
- const { resolve, promise } = openPromise<A>();
- let acc = init;
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve(acc);
- return;
- }
- acc = f(value, acc);
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
-
- forEach(f: (x: T) => void): Promise<void> {
- const { resolve, promise } = openPromise<void>();
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve();
- return;
- }
- f(value);
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
-
- run(): Promise<void> {
- const { resolve, promise } = openPromise<void>();
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve();
- return;
- }
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
+interface CursorEmptyResult<T> {
+ hasValue: false;
}
-type FilterFn = (e: any) => boolean;
-type SubscribeFn = (done: boolean, value: any, tx: IDBTransaction) => void;
-type SubscribeOneFn = (value: any, tx: IDBTransaction) => void;
-
-class QueryStreamFilter<T> extends QueryStreamBase<T> {
- constructor(public s: QueryStreamBase<T>, public filterFn: FilterFn) {
- super(s.root);
- }
-
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
- }
- if (this.filterFn(value)) {
- f(false, value, tx);
- }
- });
- }
+interface CursorValueResult<T> {
+ hasValue: true;
+ value: T;
}
-class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
- constructor(public s: QueryStreamBase<T>, public flatMapFn: (v: T) => S[]) {
- super(s.root);
- }
-
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+class ResultStream<T> {
+ private currentPromise: Promise<void>;
+ private gotCursorEnd: boolean = false;
+ private awaitingResult: boolean = false;
+
+ constructor(private req: IDBRequest) {
+ this.awaitingResult = true;
+ let p = openPromise<void>();
+ this.currentPromise = p.promise;
+ req.onsuccess = () => {
+ if (!this.awaitingResult) {
+ throw Error("BUG: invariant violated");
}
- const values = this.flatMapFn(value);
- for (const v in values) {
- f(false, v, tx);
+ const cursor = req.result;
+ if (cursor) {
+ this.awaitingResult = false;
+ p.resolve();
+ p = openPromise<void>();
+ this.currentPromise = p.promise;
+ } else {
+ this.gotCursorEnd = true;
+ p.resolve();
}
- });
- }
-}
-
-class QueryStreamMap<S, T> extends QueryStreamBase<T> {
- constructor(public s: QueryStreamBase<S>, public mapFn: (v: S) => T) {
- super(s.root);
+ };
+ req.onerror = () => {
+ p.reject(req.error);
+ };
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+ async toArray(): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(x.value);
+ } else {
+ break;
}
- const mappedValue = this.mapFn(value);
- f(false, mappedValue, tx);
- });
- }
-}
-
-class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
- constructor(
- public s: QueryStreamBase<T>,
- public storeName: string,
- public indexName: string,
- public key: any,
- ) {
- super(s.root);
+ }
+ return arr;
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+ async map<R>(f: (x: T) => R): Promise<R[]> {
+ const arr: R[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(f(x.value));
+ } else {
+ break;
}
- const joinKey = this.key(value);
- const s = tx.objectStore(this.storeName).index(this.indexName);
- const req = s.openCursor(IDBKeyRange.only(joinKey));
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- f(false, { left: value, right: cursor.value }, tx);
- cursor.continue();
- }
- };
- });
- }
-}
-
-class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<
- JoinLeftResult<T, S>
-> {
- constructor(
- public s: QueryStreamBase<T>,
- public storeName: string,
- public indexName: string,
- public key: any,
- ) {
- super(s.root);
+ }
+ return arr;
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+ async forEach(f: (x: T) => void): Promise<void> {
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ f(x.value);
+ } else {
+ break;
}
- const s = tx.objectStore(this.storeName).index(this.indexName);
- const req = s.openCursor(IDBKeyRange.only(this.key(value)));
- let gotMatch = false;
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- gotMatch = true;
- f(false, { left: value, right: cursor.value }, tx);
- cursor.continue();
- } else {
- if (!gotMatch) {
- f(false, { left: value }, tx);
- }
- }
- };
- });
- }
-}
-
-class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
- constructor(
- public s: QueryStreamBase<T>,
- public storeName: string,
- public key: any,
- ) {
- super(s.root);
+ }
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
- }
- const s = tx.objectStore(this.storeName);
- const req = s.openCursor(IDBKeyRange.only(this.key(value)));
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- f(false, { left: value, right: cursor.value }, tx);
- cursor.continue();
- } else {
- f(true, undefined, tx);
+ async filter(f: (x: T) => boolean): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ if (f(x.value)) {
+ arr.push(x.value)
}
- };
- });
- }
-}
-
-class IterQueryStream<T> extends QueryStreamBase<T> {
- private storeName: string;
- private options: any;
- private subscribers: SubscribeFn[];
-
- constructor(qr: QueryRoot, storeName: string, options: any) {
- super(qr);
- this.options = options;
- this.storeName = storeName;
- this.subscribers = [];
-
- const doIt = (tx: IDBTransaction) => {
- const { indexName = void 0, only = void 0 } = this.options;
- let s: any;
- if (indexName !== void 0) {
- s = tx.objectStore(this.storeName).index(this.options.indexName);
} else {
- s = tx.objectStore(this.storeName);
- }
- let kr: IDBKeyRange | undefined;
- if (only !== undefined) {
- kr = IDBKeyRange.only(this.options.only);
+ break;
}
- const req = s.openCursor(kr);
- req.onsuccess = () => {
- const cursor: IDBCursorWithValue = req.result;
- if (cursor) {
- for (const f of this.subscribers) {
- f(false, cursor.value, tx);
- }
- cursor.continue();
- } else {
- for (const f of this.subscribers) {
- f(true, undefined, tx);
- }
- }
- };
- };
-
- this.root.addWork(doIt);
+ }
+ return arr;
}
- subscribe(f: SubscribeFn) {
- this.subscribers.push(f);
+ async next(): Promise<CursorResult<T>> {
+ if (this.gotCursorEnd) {
+ return { hasValue: false };
+ }
+ if (!this.awaitingResult) {
+ const cursor = this.req.result;
+ if (!cursor) {
+ throw Error("assertion failed");
+ }
+ this.awaitingResult = true;
+ cursor.continue();
+ }
+ await this.currentPromise;
+ if (this.gotCursorEnd) {
+ return { hasValue: false };
+ }
+ const cursor = this.req.result;
+ if (!cursor) {
+ throw Error("assertion failed");
+ }
+ return { hasValue: true, value: cursor.value };
}
}
-/**
- * Root wrapper around an IndexedDB for queries with a fluent interface.
- */
-export class QueryRoot {
- private work: Array<(t: IDBTransaction) => void> = [];
- private stores: Set<string> = new Set();
- private kickoffPromise: Promise<void>;
-
- /**
- * Some operations is a write operation,
- * and we need to do a "readwrite" transaction/
- */
- private hasWrite: boolean;
-
- private finishScheduled: boolean;
-
- private finished: boolean = false;
+export function oneShotIter<T>(
+ db: IDBDatabase,
+ store: Store<T>
+): ResultStream<T> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).openCursor();
+ return new ResultStream<T>(req);
+}
- private keys: { [keyName: string]: IDBValidKey } = {};
+export function oneShotIterIndex<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ query?: any,
+): ResultStream<T> {
+ const tx = db.transaction([index.storeName], "readonly");
+ const req = tx.objectStore(index.storeName).index(index.indexName).openCursor(query);
+ return new ResultStream<T>(req);
+}
- constructor(public db: IDBDatabase) {}
+class TransactionHandle {
+ constructor(private tx: IDBTransaction) {}
- /**
- * Get a named key that was created during the query.
- */
- key(keyName: string): IDBValidKey | undefined {
- return this.keys[keyName];
+ put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
}
- private checkFinished() {
- if (this.finished) {
- throw Error("Can't add work to query after it was started");
- }
- }
-
- /**
- * Get a stream of all objects in the store.
- */
- iter<T>(store: Store<T>): QueryStream<T> {
- this.checkFinished();
- this.stores.add(store.name);
- this.scheduleFinish();
- return new IterQueryStream<T>(this, store.name, {});
+ add<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).add(value, key);
+ return requestToPromise(req);
}
- /**
- * Count the number of objects in a store.
- */
- count<T>(store: Store<T>): Promise<number> {
- this.checkFinished();
- const { resolve, promise } = openPromise<number>();
-
- const doCount = (tx: IDBTransaction) => {
- const s = tx.objectStore(store.name);
- const req = s.count();
- req.onsuccess = () => {
- resolve(req.result);
- };
- };
-
- this.addWork(doCount, store.name, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
-
- /**
- * Delete all objects in a store that match a predicate.
- */
- deleteIf<T>(
- store: Store<T>,
- predicate: (x: T, n: number) => boolean,
- ): QueryRoot {
- this.checkFinished();
- const doDeleteIf = (tx: IDBTransaction) => {
- const s = tx.objectStore(store.name);
- const req = s.openCursor();
- let n = 0;
- req.onsuccess = () => {
- const cursor: IDBCursorWithValue | null = req.result;
- if (cursor) {
- if (predicate(cursor.value, n++)) {
- cursor.delete();
- }
- cursor.continue();
- }
- };
- };
- this.addWork(doDeleteIf, store.name, true);
- return this;
+ get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const req = this.tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
}
- iterIndex<S extends IDBValidKey, T>(
- index: Index<S, T>,
- only?: S,
- ): QueryStream<T> {
- this.checkFinished();
- this.stores.add(index.storeName);
- this.scheduleFinish();
- return new IterQueryStream<T>(this, index.storeName, {
- indexName: index.indexName,
- only,
- });
+ iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return new ResultStream<T>(req);
}
- /**
- * Put an object into the given object store.
- * Overrides if an existing object with the same key exists
- * in the store.
- */
- put<T>(store: Store<T>, val: T, keyName?: string): QueryRoot {
- this.checkFinished();
- const doPut = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).put(val);
- if (keyName) {
- req.onsuccess = () => {
- this.keys[keyName] = req.result;
- };
- }
- };
- this.scheduleFinish();
- this.addWork(doPut, store.name, true);
- return this;
+ delete<T>(store: Store<T>, key: any): Promise<void> {
+ const req = this.tx.objectStore(store.name).delete(key);
+ return requestToPromise(req);
}
- /**
- * Put an object into a store or return an existing record.
- */
- putOrGetExisting<T>(store: Store<T>, val: T, key: IDBValidKey): Promise<T> {
- this.checkFinished();
- const { resolve, promise } = openPromise<T>();
- const doPutOrGet = (tx: IDBTransaction) => {
- const objstore = tx.objectStore(store.name);
- const req = objstore.get(key);
- req.onsuccess = () => {
- if (req.result !== undefined) {
- resolve(req.result);
- } else {
- const req2 = objstore.add(val);
- req2.onsuccess = () => {
- resolve(val);
- };
- }
- };
- };
- this.scheduleFinish();
- this.addWork(doPutOrGet, store.name, true);
- return promise;
+ mutate<T>(store: Store<T>, key: any, f: (x: T) => T | undefined) {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
}
+}
- putWithResult<T>(store: Store<T>, val: T): Promise<IDBValidKey> {
- this.checkFinished();
- const { resolve, promise } = openPromise<IDBValidKey>();
- const doPutWithResult = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).put(val);
- req.onsuccess = () => {
- resolve(req.result);
- };
- this.scheduleFinish();
+export function runWithWriteTransaction<T>(
+ db: IDBDatabase,
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+): Promise<T> {
+ return new Promise((resolve, reject) => {
+ const storeName = stores.map(x => x.name);
+ const tx = db.transaction(storeName, "readwrite");
+ let funResult: any = undefined;
+ let gotFunResult: boolean = false;
+ tx.onerror = () => {
+ console.error("error in transaction:", tx.error);
+ reject(tx.error);
};
- this.addWork(doPutWithResult, store.name, true);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
-
- /**
- * Update objects inside a transaction.
- *
- * If the mutation function throws AbortTransaction, the whole transaction will be aborted.
- * If the mutation function returns undefined or null, no modification will be made.
- */
- mutate<T>(store: Store<T>, key: any, f: (v: T) => T | undefined): QueryRoot {
- this.checkFinished();
- const doPut = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key));
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- const value = cursor.value;
- let modifiedValue: T | undefined;
- try {
- modifiedValue = f(value);
- } catch (e) {
- if (e === AbortTransaction) {
- tx.abort();
- return;
- }
- throw e;
- }
- if (modifiedValue !== undefined && modifiedValue !== null) {
- cursor.update(modifiedValue);
- }
- cursor.continue();
- }
- };
- };
- this.scheduleFinish();
- this.addWork(doPut, store.name, true);
- return this;
- }
-
- /**
- * Add all object from an iterable to the given object store.
- */
- putAll<T>(store: Store<T>, iterable: T[]): QueryRoot {
- this.checkFinished();
- const doPutAll = (tx: IDBTransaction) => {
- for (const obj of iterable) {
- tx.objectStore(store.name).put(obj);
+ tx.oncomplete = () => {
+ // This is a fatal error: The transaction completed *before*
+ // the transaction function returned. Likely, the transaction
+ // function waited on a promise that is *not* resolved in the
+ // microtask queue, thus triggering the auto-commit behavior.
+ // Unfortunately, the auto-commit behavior of IDB can't be switched
+ // of. There are some proposals to add this functionality in the future.
+ if (!gotFunResult) {
+ const msg =
+ "BUG: transaction closed before transaction function returned";
+ console.error(msg);
+ reject(Error(msg));
}
+ resolve(funResult);
};
- this.scheduleFinish();
- this.addWork(doPutAll, store.name, true);
- return this;
- }
-
- /**
- * Add an object to the given object store.
- * Fails if the object's key is already present
- * in the object store.
- */
- add<T>(store: Store<T>, val: T): QueryRoot {
- this.checkFinished();
- const doAdd = (tx: IDBTransaction) => {
- tx.objectStore(store.name).add(val);
+ tx.onabort = () => {
+ console.error("aborted transaction");
+ reject(AbortTransaction);
};
- this.scheduleFinish();
- this.addWork(doAdd, store.name, true);
- return this;
- }
-
- /**
- * Get one object from a store by its key.
- */
- get<T>(store: Store<T>, key: any): Promise<T | undefined> {
- this.checkFinished();
- if (key === void 0) {
- throw Error("key must not be undefined");
- }
-
- const { resolve, promise } = openPromise<T | undefined>();
-
- const doGet = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).get(key);
- req.onsuccess = () => {
- resolve(req.result);
- };
- };
-
- this.addWork(doGet, store.name, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
+ const th = new TransactionHandle(tx);
+ const resP = f(th);
+ resP.then(result => {
+ gotFunResult = true;
+ funResult = result;
+ });
+ });
+}
+/**
+ * Definition of an index.
+ */
+export class Index<S extends IDBValidKey, T> {
/**
- * Get get objects from a store by their keys.
- * If no object for a key exists, the resulting position in the array
- * contains 'undefined'.
+ * Name of the store that this index is associated with.
*/
- getMany<T>(store: Store<T>, keys: any[]): Promise<T[]> {
- this.checkFinished();
-
- const { resolve, promise } = openPromise<T[]>();
- const results: T[] = [];
-
- const doGetMany = (tx: IDBTransaction) => {
- for (const key of keys) {
- if (key === void 0) {
- throw Error("key must not be undefined");
- }
- const req = tx.objectStore(store.name).get(key);
- req.onsuccess = () => {
- results.push(req.result);
- if (results.length === keys.length) {
- resolve(results);
- }
- };
- }
- };
-
- this.addWork(doGetMany, store.name, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
+ storeName: string;
/**
- * Get one object from a store by its key.
+ * Options to use for the index.
*/
- getIndexed<I extends IDBValidKey, T>(
- index: Index<I, T>,
- key: I,
- ): Promise<T | undefined> {
- this.checkFinished();
- if (key === void 0) {
- throw Error("key must not be undefined");
- }
-
- const { resolve, promise } = openPromise<T | undefined>();
+ options: IndexOptions;
- const doGetIndexed = (tx: IDBTransaction) => {
- const req = tx
- .objectStore(index.storeName)
- .index(index.indexName)
- .get(key);
- req.onsuccess = () => {
- resolve(req.result);
- };
+ constructor(
+ s: Store<T>,
+ public indexName: string,
+ public keyPath: string | string[],
+ options?: IndexOptions,
+ ) {
+ const defaultOptions = {
+ multiEntry: false,
};
-
- this.addWork(doGetIndexed, index.storeName, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
-
- private scheduleFinish() {
- if (!this.finishScheduled) {
- Promise.resolve().then(() => this.finish());
- this.finishScheduled = true;
- }
- }
-
- /**
- * Finish the query, and start the query in the first place if necessary.
- */
- finish(): Promise<void> {
- if (this.kickoffPromise) {
- return this.kickoffPromise;
- }
- this.kickoffPromise = new Promise<void>((resolve, reject) => {
- // At this point, we can't add any more work
- this.finished = true;
- if (this.work.length === 0) {
- resolve();
- return;
- }
- const mode = this.hasWrite ? "readwrite" : "readonly";
- const tx = this.db.transaction(Array.from(this.stores), mode);
- tx.oncomplete = () => {
- resolve();
- };
- tx.onabort = () => {
- console.warn(
- `aborted ${mode} transaction on stores [${[...this.stores]}]`,
- );
- reject(Error("transaction aborted"));
- };
- tx.onerror = e => {
- console.warn(`error in transaction`, (e.target as any).error);
- };
- for (const w of this.work) {
- w(tx);
- }
- });
- return this.kickoffPromise;
+ this.options = { ...defaultOptions, ...(options || {}) };
+ this.storeName = s.name;
}
/**
- * Delete an object by from the given object store.
+ * We want to have the key type parameter in use somewhere,
+ * because otherwise the compiler complains. In iterIndex the
+ * key type is pretty useful.
*/
- delete<T>(store: Store<T>, key: any): QueryRoot {
- this.checkFinished();
- const doDelete = (tx: IDBTransaction) => {
- tx.objectStore(store.name).delete(key);
- };
- this.scheduleFinish();
- this.addWork(doDelete, store.name, true);
- return this;
- }
+ protected _dummyKey: S | undefined;
+}
- /**
- * Low-level function to add a task to the internal work queue.
- */
- addWork(
- workFn: (t: IDBTransaction) => void,
- storeName?: string,
- isWrite?: boolean,
- ) {
- this.work.push(workFn);
- if (storeName) {
- this.addStoreAccess(storeName, isWrite);
- }
- }
- addStoreAccess(storeName: string, isWrite?: boolean) {
- if (storeName) {
- this.stores.add(storeName);
- }
- if (isWrite) {
- this.hasWrite = true;
- }
- }
-}
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const AbortTransaction = Symbol("abort_transaction");
diff --git a/src/wallet.ts b/src/wallet.ts
index f5219c459..32b0833cd 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -29,14 +29,19 @@ import {
canonicalizeBaseUrl,
getTalerStampSec,
strcmp,
+ extractTalerStamp,
} from "./helpers";
import { HttpRequestLibrary, RequestException } from "./http";
import * as LibtoolVersion from "./libtoolVersion";
import {
AbortTransaction,
- JoinLeftResult,
- JoinResult,
- QueryRoot,
+ oneShotPut,
+ oneShotGet,
+ runWithWriteTransaction,
+ oneShotIter,
+ oneShotIterIndex,
+ oneShotGetIndexed,
+ oneShotMutate,
} from "./query";
import { TimerGroup } from "./timer";
@@ -63,6 +68,8 @@ import {
TipRecord,
WireFee,
WithdrawalRecord,
+ ExchangeDetails,
+ ExchangeUpdateStatus,
} from "./dbTypes";
import {
Auditor,
@@ -110,6 +117,8 @@ import {
PendingOperationInfo,
PendingOperationsResponse,
HistoryQuery,
+ getTimestampNow,
+ OperationError,
} from "./walletTypes";
import { openPromise } from "./promiseUtils";
import {
@@ -339,6 +348,18 @@ interface CoinsForPaymentArgs {
}
/**
+ * This error is thrown when an
+ */
+class OperationFailedAndReportedError extends Error {
+ constructor(public reason: Error) {
+ super("Reported failed operation: " + reason.message);
+
+ // Set the prototype explicitly.
+ Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
+ }
+}
+
+/**
* The platform-independent wallet implementation.
*/
export class Wallet {
@@ -372,10 +393,6 @@ export class Wallet {
*/
private runningOperations: Set<string> = new Set();
- q(): QueryRoot {
- return new QueryRoot(this.db);
- }
-
constructor(
db: IDBDatabase,
http: HttpRequestLibrary,
@@ -389,31 +406,49 @@ export class Wallet {
this.notifier = notifier;
this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
this.timerGroup = new TimerGroup();
+ }
- const init = async () => {
- await this.fillDefaults().catch(e => console.log(e));
+ public async processPending(): Promise<void> {
+
+ const exchangeBaseUrlList = await oneShotIter(this.db, Stores.exchanges).map((x) => x.baseUrl);
+
+ for (let exchangeBaseUrl of exchangeBaseUrlList) {
+ await this.updateExchangeFromUrl(exchangeBaseUrl);
+ }
+ }
+
+ /**
+ * Start processing pending operations asynchronously.
+ */
+ public start() {
+ const work = async () => {
await this.collectGarbage().catch(e => console.log(e));
this.updateExchanges();
this.resumePendingFromDb();
this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges());
};
-
- init();
+ work();
}
- private async fillDefaults() {
- const onTrue = (r: QueryRoot) => {};
- const onFalse = (r: QueryRoot) => {
- Wallet.enableTracing && console.log("applying defaults");
- r.put(Stores.config, { key: "currencyDefaultsApplied", value: true })
- .putAll(Stores.currencies, builtinCurrencies)
- .finish();
- };
- await this.q()
- .iter(Stores.config)
- .filter(x => x.key === "currencyDefaultsApplied")
- .first()
- .cond(x => x && x.value, onTrue, onFalse);
+ /**
+ * Insert the hard-coded defaults for exchanges, coins and
+ * auditors into the database, unless these defaults have
+ * already been applied.
+ */
+ async fillDefaults() {
+ await runWithWriteTransaction(this.db, [Stores.config, Stores.currencies], async (tx) => {
+ let applied = false;
+ await tx.iter(Stores.config).forEach((x) => {
+ if (x.key == "currencyDefaultsApplied" && x.value == true) {
+ applied = true;
+ }
+ });
+ if (!applied) {
+ for (let c of builtinCurrencies) {
+ await tx.put(Stores.currencies, c);
+ }
+ }
+ });
}
private startOperation(operationId: string) {
@@ -429,12 +464,9 @@ export class Wallet {
}
async updateExchanges(): Promise<void> {
- const exchangesUrls = await this.q()
- .iter(Stores.exchanges)
- .map(e => e.baseUrl)
- .toArray();
+ const exchangeUrls = await oneShotIter(this.db, Stores.exchanges).map((e) => e.baseUrl);
- for (const url of exchangesUrls) {
+ for (const url of exchangeUrls) {
this.updateExchangeFromUrl(url).catch(e => {
console.error("updating exchange failed", e);
});
@@ -448,69 +480,46 @@ export class Wallet {
private resumePendingFromDb(): void {
Wallet.enableTracing && console.log("resuming pending operations from db");
- this.q()
- .iter(Stores.reserves)
- .forEach(reserve => {
+ oneShotIter(this.db, Stores.reserves).forEach(reserve => {
Wallet.enableTracing &&
console.log("resuming reserve", reserve.reserve_pub);
this.processReserve(reserve.reserve_pub);
- });
-
- this.q()
- .iter(Stores.precoins)
- .forEach(preCoin => {
- Wallet.enableTracing && console.log("resuming precoin");
- this.processPreCoin(preCoin.coinPub);
- });
+ });
- this.q()
- .iter(Stores.refresh)
- .forEach((r: RefreshSessionRecord) => {
- this.continueRefreshSession(r);
- });
+ oneShotIter(this.db, Stores.precoins).forEach(preCoin => {
+ Wallet.enableTracing && console.log("resuming precoin");
+ this.processPreCoin(preCoin.coinPub);
+ });
- this.q()
- .iter(Stores.coinsReturns)
- .forEach((r: CoinsReturnRecord) => {
- this.depositReturnedCoins(r);
- });
+ oneShotIter(this.db, Stores.refresh).forEach((r: RefreshSessionRecord) => {
+ this.continueRefreshSession(r);
+ });
- // FIXME: optimize via index
- this.q()
- .iter(Stores.coins)
- .forEach((c: CoinRecord) => {
- if (c.status === CoinStatus.Dirty) {
- Wallet.enableTracing &&
- console.log("resuming pending refresh for coin", c);
- this.refresh(c.coinPub);
- }
- });
+ oneShotIter(this.db, Stores.coinsReturns).forEach((r: CoinsReturnRecord) => {
+ this.depositReturnedCoins(r);
+ });
}
private async getCoinsForReturn(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<CoinWithDenom[] | undefined> {
- const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+ const exchange = await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
if (!exchange) {
throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
}
- const coins: CoinRecord[] = await this.q()
- .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
- .toArray();
+ const coins: CoinRecord[] = await oneShotIterIndex(this.db, Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
if (!coins || !coins.length) {
return [];
}
- const denoms = await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
- .toArray();
+ const denoms = await oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
// Denomination of the first coin, we assume that all other
// coins have the same currency
- const firstDenom = await this.q().get(Stores.denominations, [
+ const firstDenom = await oneShotGet(this.db, Stores.denominations, [
exchange.baseUrl,
coins[0].denomPub,
]);
@@ -521,7 +530,7 @@ export class Wallet {
const cds: CoinWithDenom[] = [];
for (const coin of coins) {
- const denom = await this.q().get(Stores.denominations, [
+ const denom = await oneShotGet(this.db, Stores.denominations, [
exchange.baseUrl,
coin.denomPub,
]);
@@ -572,16 +581,22 @@ export class Wallet {
let remainingAmount = paymentAmount;
- const exchanges = await this.q()
- .iter(Stores.exchanges)
- .toArray();
+ const exchanges = await oneShotIter(this.db, Stores.exchanges).toArray();
for (const exchange of exchanges) {
let isOkay: boolean = false;
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ continue;
+ }
+ const exchangeFees = exchange.wireInfo;
+ if (!exchangeFees) {
+ continue;
+ }
// is the exchange explicitly allowed?
for (const allowedExchange of allowedExchanges) {
- if (allowedExchange.master_pub === exchange.masterPublicKey) {
+ if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
isOkay = true;
break;
}
@@ -590,7 +605,7 @@ export class Wallet {
// is the exchange allowed because of one of its auditors?
if (!isOkay) {
for (const allowedAuditor of allowedAuditors) {
- for (const auditor of exchange.auditors) {
+ for (const auditor of exchangeDetails.auditors) {
if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
isOkay = true;
break;
@@ -606,20 +621,17 @@ export class Wallet {
continue;
}
- const coins: CoinRecord[] = await this.q()
- .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
- .toArray();
+ const coins = await oneShotIterIndex(this.db, Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
+
+ const denoms = await oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
- const denoms = await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
- .toArray();
if (!coins || coins.length === 0) {
continue;
}
// Denomination of the first coin, we assume that all other
// coins have the same currency
- const firstDenom = await this.q().get(Stores.denominations, [
+ const firstDenom = await oneShotGet(this.db, Stores.denominations, [
exchange.baseUrl,
coins[0].denomPub,
]);
@@ -629,7 +641,7 @@ export class Wallet {
const currency = firstDenom.value.currency;
const cds: CoinWithDenom[] = [];
for (const coin of coins) {
- const denom = await this.q().get(Stores.denominations, [
+ const denom = await oneShotGet(this.db, Stores.denominations, [
exchange.baseUrl,
coin.denomPub,
]);
@@ -651,18 +663,9 @@ export class Wallet {
cds.push({ coin, denom });
}
- const fees = await this.q().get(
- Stores.exchangeWireFees,
- exchange.baseUrl,
- );
- if (!fees) {
- console.error("no fees found for exchange", exchange);
- continue;
- }
-
let totalFees = Amounts.getZero(currency);
let wireFee: AmountJson | undefined;
- for (const fee of fees.feesForType[wireMethod] || []) {
+ for (const fee of exchangeFees.feesForType[wireMethod] || []) {
if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
wireFee = fee.wireFee;
break;
@@ -723,10 +726,13 @@ export class Wallet {
timestamp_refund: 0,
};
- await this.q()
- .put(Stores.purchases, t)
- .putAll(Stores.coins, payCoinInfo.updatedCoins)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.coins, Stores.purchases], async (tx) => {
+ await tx.put(Stores.purchases, t);
+ for (let c of payCoinInfo.updatedCoins) {
+ await tx.put(Stores.coins, c);
+ }
+ });
+
this.badge.showNotification();
this.notifier.notify();
return t;
@@ -773,7 +779,8 @@ export class Wallet {
console.log("proposal", proposal);
- const differentPurchase = await this.q().getIndexed(
+ const differentPurchase = await oneShotGetIndexed(
+ this.db,
Stores.purchases.fulfillmentUrlIndex,
proposal.contractTerms.fulfillment_url,
);
@@ -805,10 +812,7 @@ export class Wallet {
}
// First check if we already payed for it.
- const purchase = await this.q().get(
- Stores.purchases,
- proposal.contractTermsHash,
- );
+ const purchase = await oneShotGet(this.db, Stores.purchases, proposal.contractTermsHash);
if (!purchase) {
const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
@@ -890,10 +894,7 @@ export class Wallet {
* downloaded in the context of a session ID.
*/
async downloadProposal(url: string, sessionId?: string): Promise<number> {
- const oldProposal = await this.q().getIndexed(
- Stores.proposals.urlIndex,
- url,
- );
+ const oldProposal = await oneShotGetIndexed(this.db, Stores.proposals.urlIndex, url);
if (oldProposal) {
return oldProposal.id!;
}
@@ -924,7 +925,7 @@ export class Wallet {
downloadSessionId: sessionId,
};
- const id = await this.q().putWithResult(Stores.proposals, proposalRecord);
+ const id = await oneShotPut(this.db, Stores.proposals, proposalRecord);
this.notifier.notify();
if (typeof id !== "number") {
throw Error("db schema wrong");
@@ -934,19 +935,15 @@ export class Wallet {
async refundFailedPay(proposalId: number) {
console.log(`refunding failed payment with proposal id ${proposalId}`);
- const proposal: ProposalDownloadRecord | undefined = await this.q().get(
- Stores.proposals,
- proposalId,
- );
-
+ const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
if (!proposal) {
throw Error(`proposal with id ${proposalId} not found`);
}
- const purchase = await this.q().get(
+ const purchase = await oneShotGet(this.db,
Stores.purchases,
- proposal.contractTermsHash,
- );
+ proposal.contractTermsHash);
+
if (!purchase) {
throw Error("purchase not found for proposal");
}
@@ -960,7 +957,7 @@ export class Wallet {
contractTermsHash: string,
sessionId: string | undefined,
): Promise<ConfirmPayResult> {
- const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+ const purchase = await oneShotGet(this.db, Stores.purchases, contractTermsHash);
if (!purchase) {
throw Error("Purchase not found: " + contractTermsHash);
}
@@ -998,7 +995,7 @@ export class Wallet {
purchase.finished = true;
const modifiedCoins: CoinRecord[] = [];
for (const pc of purchase.payReq.coins) {
- const c = await this.q().get<CoinRecord>(Stores.coins, pc.coin_pub);
+ const c = await oneShotGet(this.db, Stores.coins, pc.coin_pub);
if (!c) {
console.error("coin not found");
throw Error("coin used in payment not found");
@@ -1007,10 +1004,13 @@ export class Wallet {
modifiedCoins.push(c);
}
- await this.q()
- .putAll(Stores.coins, modifiedCoins)
- .put(Stores.purchases, purchase)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.coins, Stores.purchases], async (tx) => {
+ for (let c of modifiedCoins) {
+ tx.put(Stores.coins, c);
+ }
+ tx.put(Stores.purchases, purchase);
+ });
+
for (const c of purchase.payReq.coins) {
this.refresh(c.coin_pub);
}
@@ -1031,9 +1031,7 @@ export class Wallet {
*/
async refreshDirtyCoins(): Promise<{ numRefreshed: number }> {
let n = 0;
- const coins = await this.q()
- .iter(Stores.coins)
- .toArray();
+ const coins = await oneShotIter(this.db, Stores.coins).toArray();
for (let coin of coins) {
if (coin.status == CoinStatus.Dirty) {
try {
@@ -1059,10 +1057,7 @@ export class Wallet {
console.log(
`executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
);
- const proposal: ProposalDownloadRecord | undefined = await this.q().get(
- Stores.proposals,
- proposalId,
- );
+ const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
if (!proposal) {
throw Error(`proposal with id ${proposalId} not found`);
@@ -1070,10 +1065,9 @@ export class Wallet {
const sessionId = sessionIdOverride || proposal.downloadSessionId;
- let purchase = await this.q().get(
+ let purchase = await oneShotGet(this.db,
Stores.purchases,
- proposal.contractTermsHash,
- );
+ proposal.contractTermsHash,);
if (purchase) {
return this.submitPay(purchase.contractTermsHash, sessionId);
@@ -1145,7 +1139,13 @@ export class Wallet {
return;
}
const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
- const coins = await this.q().getMany(Stores.coins, coinKeys);
+ const coins: CoinRecord[] = [];
+ for (let coinKey of coinKeys) {
+ const cc = await oneShotGet(this.db, Stores.coins, coinKey);
+ if (cc) {
+ coins.push(cc);
+ }
+ }
for (let i = 0; i < coins.length; i++) {
const specCoin = sp.payCoinInfo.originalCoins[i];
const currentCoin = coins[i];
@@ -1164,13 +1164,12 @@ export class Wallet {
}
/**
- * Send reserve details
+ * Send reserve details
*/
private async sendReserveInfoToBank(reservePub: string) {
- const reserve = await this.q().get<ReserveRecord>(
+ const reserve = await oneShotGet(this.db,
Stores.reserves,
- reservePub,
- );
+ reservePub);
if (!reserve) {
throw Error("reserve not in db");
}
@@ -1191,7 +1190,7 @@ export class Wallet {
}
if (status.transfer_done) {
- await this.q().mutate(Stores.reserves, reservePub, r => {
+ await oneShotMutate(this.db, Stores.reserves, reservePub, (r) => {
r.timestamp_confirmed = now;
return r;
});
@@ -1207,7 +1206,7 @@ export class Wallet {
console.log("bank error response", e);
throw e;
}
- await this.q().mutate(Stores.reserves, reservePub, r => {
+ await oneShotMutate(this.db, Stores.reserves, reservePub, (r) => {
r.timestamp_reserve_info_posted = now;
return r;
});
@@ -1238,10 +1237,7 @@ export class Wallet {
// Sometimes though, we want to try again faster.
let maxTimeout = 3000 * 60;
try {
- const reserve = await this.q().get<ReserveRecord>(
- Stores.reserves,
- reservePub,
- );
+ const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
if (!reserve) {
isHardError = true;
throw Error("reserve not in db");
@@ -1302,7 +1298,7 @@ export class Wallet {
const op = openPromise<void>();
const processPreCoinInternal = async (retryDelayMs: number = 200) => {
- const preCoin = await this.q().get(Stores.precoins, preCoinPub);
+ const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
if (!preCoin) {
console.log("processPreCoin: preCoinPub not found");
return;
@@ -1325,15 +1321,14 @@ export class Wallet {
this.processPreCoinConcurrent++;
try {
- const exchange = await this.q().get(
+ const exchange = await oneShotGet(this.db,
Stores.exchanges,
- preCoin.exchangeBaseUrl,
- );
+ preCoin.exchangeBaseUrl,);
if (!exchange) {
console.error("db inconsistent: exchange for precoin not found");
return;
}
- const denom = await this.q().get(Stores.denominations, [
+ const denom = await oneShotGet(this.db, Stores.denominations, [
preCoin.exchangeBaseUrl,
preCoin.denomPub,
]);
@@ -1358,11 +1353,11 @@ export class Wallet {
return r;
};
- await this.q()
- .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
- .delete(Stores.precoins, coin.coinPub)
- .add(Stores.coins, coin)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.reserves, Stores.precoins, Stores.coins], async (tx) => {
+ await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
+ await tx.delete(Stores.precoins, coin.coinPub);
+ await tx.add(Stores.coins, coin);
+ });
this.badge.showNotification();
@@ -1403,20 +1398,6 @@ export class Wallet {
}
/**
- * Update the timestamp of when an exchange was used.
- */
- async updateExchangeUsedTime(exchangeBaseUrl: string): Promise<void> {
- const now = new Date().getTime();
- const update = (r: ExchangeRecord) => {
- r.lastUsedTime = now;
- return r;
- };
- await this.q()
- .mutate(Stores.exchanges, exchangeBaseUrl, update)
- .finish();
- }
-
- /**
* Create a reserve, but do not flag it as confirmed yet.
*
* Adds the corresponding exchange as a trusted exchange if it is neither
@@ -1451,38 +1432,38 @@ export class Wallet {
const rec = {
paytoUri: senderWire,
};
- await this.q()
- .put(Stores.senderWires, rec)
- .finish();
+ await oneShotPut(this.db, Stores.senderWires, rec);
}
- await this.updateExchangeUsedTime(req.exchange);
const exchangeInfo = await this.updateExchangeFromUrl(req.exchange);
+ const exchangeDetails = exchangeInfo.details;
+ if (!exchangeDetails) {
+ throw Error("exchange not updated");
+ }
const { isAudited, isTrusted } = await this.getExchangeTrust(exchangeInfo);
- let currencyRecord = await this.q().get(
- Stores.currencies,
- exchangeInfo.currency,
- );
+ let currencyRecord = await oneShotGet(this.db, Stores.currencies, exchangeDetails.currency);
if (!currencyRecord) {
currencyRecord = {
auditors: [],
exchanges: [],
fractionalDigits: 2,
- name: exchangeInfo.currency,
+ name: exchangeDetails.currency,
};
}
if (!isAudited && !isTrusted) {
currencyRecord.exchanges.push({
baseUrl: req.exchange,
- exchangePub: exchangeInfo.masterPublicKey,
+ exchangePub: exchangeDetails.masterPublicKey,
});
}
- await this.q()
- .put(Stores.currencies, currencyRecord)
- .put(Stores.reserves, reserveRecord)
- .finish();
+ const cr: CurrencyRecord = currencyRecord;
+
+ runWithWriteTransaction(this.db, [Stores.currencies, Stores.reserves], async (tx) => {
+ await tx.put(Stores.currencies, cr);
+ await tx.put(Stores.reserves, reserveRecord);
+ });
if (req.bankWithdrawStatusUrl) {
this.processReserve(keypair.pub);
@@ -1506,17 +1487,13 @@ export class Wallet {
*/
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
const now = new Date().getTime();
- const reserve: ReserveRecord | undefined = await this.q().get<
- ReserveRecord
- >(Stores.reserves, req.reservePub);
+ const reserve = await oneShotGet(this.db, Stores.reserves, req.reservePub);
if (!reserve) {
console.error("Unable to confirm reserve, not found in DB");
return;
}
reserve.timestamp_confirmed = now;
- await this.q()
- .put(Stores.reserves, reserve)
- .finish();
+ await oneShotPut(this.db, Stores.reserves, reserve);
this.notifier.notify();
this.processReserve(reserve.reserve_pub);
@@ -1589,22 +1566,33 @@ export class Wallet {
reservePub: reserve.reserve_pub,
withdrawalAmount: Amounts.toString(withdrawAmount),
startTimestamp: stampMsNow,
- }
+ };
- const preCoinRecords: PreCoinRecord[] = await Promise.all(denomsForWithdraw.map(async denom => {
- return await this.cryptoApi.createPreCoin(denom, reserve);
- }));
+ const preCoinRecords: PreCoinRecord[] = await Promise.all(
+ denomsForWithdraw.map(async denom => {
+ return await this.cryptoApi.createPreCoin(denom, reserve);
+ }),
+ );
- const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value)).amount
- const totalCoinWithdrawFee = Amounts.sum(denomsForWithdraw.map(x => x.feeWithdraw)).amount
- const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee).amount
+ const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
+ .amount;
+ const totalCoinWithdrawFee = Amounts.sum(
+ denomsForWithdraw.map(x => x.feeWithdraw),
+ ).amount;
+ const totalWithdrawAmount = Amounts.add(
+ totalCoinValue,
+ totalCoinWithdrawFee,
+ ).amount;
function mutateReserve(r: ReserveRecord): ReserveRecord {
const currentAmount = r.current_amount;
if (!currentAmount) {
throw Error("can't withdraw when amount is unknown");
}
- r.precoin_amount = Amounts.add(r.precoin_amount, totalWithdrawAmount).amount;
+ r.precoin_amount = Amounts.add(
+ r.precoin_amount,
+ totalWithdrawAmount,
+ ).amount;
const result = Amounts.sub(currentAmount, totalWithdrawAmount);
if (result.saturated) {
console.error("can't create precoins, saturated");
@@ -1623,11 +1611,13 @@ export class Wallet {
// This will fail and throw an exception if the remaining amount in the
// reserve is too low to create a pre-coin.
try {
- await this.q()
- .putAll(Stores.precoins, preCoinRecords)
- .put(Stores.withdrawals, withdrawalRecord)
- .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.precoins, Stores.withdrawals, Stores.reserves], async (tx) => {
+ for (let pcr of preCoinRecords) {
+ await tx.put(Stores.precoins, pcr);
+ }
+ await tx.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve);
+ await tx.put(Stores.withdrawals, withdrawalRecord);
+ });
} catch (e) {
return;
}
@@ -1642,10 +1632,7 @@ export class Wallet {
* by quering the reserve's exchange.
*/
private async updateReserve(reservePub: string): Promise<ReserveRecord> {
- const reserve = await this.q().get<ReserveRecord>(
- Stores.reserves,
- reservePub,
- );
+ const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
if (!reserve) {
throw Error("reserve not in db");
}
@@ -1669,44 +1656,16 @@ export class Wallet {
throw Error();
}
reserve.current_amount = Amounts.parseOrThrow(reserveInfo.balance);
- await this.q()
- .put(Stores.reserves, reserve)
- .finish();
+ await oneShotPut(this.db, Stores.reserves, reserve);
this.notifier.notify();
return reserve;
}
- /**
- * Get the wire information for the exchange with the given base URL.
- */
- async getWireInfo(exchangeBaseUrl: string): Promise<ExchangeWireJson> {
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
- const reqUrl = new URI("wire")
- .absoluteTo(exchangeBaseUrl)
- .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
- const resp = await this.http.get(reqUrl.href());
-
- if (resp.status !== 200) {
- throw Error("/wire request failed");
- }
-
- const wiJson = resp.responseJson;
- if (!wiJson) {
- throw Error("/wire response malformed");
- }
-
- return ExchangeWireJson.checked(wiJson);
- }
-
- async getPossibleDenoms(exchangeBaseUrl: string) {
- return this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
- .filter(
- d =>
- d.status === DenominationStatus.Unverified ||
- d.status === DenominationStatus.VerifiedGood,
- )
- .toArray();
+ async getPossibleDenoms(exchangeBaseUrl: string): Promise<DenominationRecord[]> {
+ return await oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl).filter((d) => {
+ return d.status === DenominationStatus.Unverified ||
+ d.status === DenominationStatus.VerifiedGood;
+ });
}
/**
@@ -1718,19 +1677,17 @@ export class Wallet {
async getVerifiedSmallestWithdrawAmount(
exchangeBaseUrl: string,
): Promise<AmountJson> {
- const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+ const exchange = await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
if (!exchange) {
throw Error(`exchange ${exchangeBaseUrl} not found`);
}
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ throw Error(`exchange ${exchangeBaseUrl} details not available`);
+ }
+
+ const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
- const possibleDenoms = await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
- .filter(
- d =>
- d.status === DenominationStatus.Unverified ||
- d.status === DenominationStatus.VerifiedGood,
- )
- .toArray();
possibleDenoms.sort((d1, d2) => {
const a1 = Amounts.add(d1.feeWithdraw, d1.value).amount;
const a2 = Amounts.add(d2.feeWithdraw, d2.value).amount;
@@ -1743,21 +1700,19 @@ export class Wallet {
}
const valid = await this.cryptoApi.isValidDenom(
denom,
- exchange.masterPublicKey,
+ exchangeDetails.masterPublicKey,
);
if (!valid) {
denom.status = DenominationStatus.VerifiedBad;
} else {
denom.status = DenominationStatus.VerifiedGood;
}
- await this.q()
- .put(Stores.denominations, denom)
- .finish();
+ await oneShotPut(this.db, Stores.denominations, denom);
if (valid) {
return Amounts.add(denom.feeWithdraw, denom.value).amount;
}
}
- return Amounts.getZero(exchange.currency);
+ return Amounts.getZero(exchangeDetails.currency);
}
/**
@@ -1771,19 +1726,16 @@ export class Wallet {
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<DenominationRecord[]> {
- const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+ const exchange = await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
if (!exchange) {
throw Error(`exchange ${exchangeBaseUrl} not found`);
}
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ throw Error(`exchange ${exchangeBaseUrl} details not available`);
+ }
- const possibleDenoms = await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
- .filter(
- d =>
- d.status === DenominationStatus.Unverified ||
- d.status === DenominationStatus.VerifiedGood,
- )
- .toArray();
+ const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
let allValid = false;
@@ -1797,7 +1749,7 @@ export class Wallet {
if (denom.status === DenominationStatus.Unverified) {
const valid = await this.cryptoApi.isValidDenom(
denom,
- exchange.masterPublicKey,
+ exchangeDetails.masterPublicKey,
);
if (!valid) {
denom.status = DenominationStatus.VerifiedBad;
@@ -1806,9 +1758,7 @@ export class Wallet {
denom.status = DenominationStatus.VerifiedGood;
nextPossibleDenoms.push(denom);
}
- await this.q()
- .put(Stores.denominations, denom)
- .finish();
+ await oneShotPut(this.db, Stores.denominations, denom);
} else {
nextPossibleDenoms.push(denom);
}
@@ -1826,19 +1776,22 @@ export class Wallet {
): Promise<{ isTrusted: boolean; isAudited: boolean }> {
let isTrusted = false;
let isAudited = false;
- const currencyRecord = await this.q().get(
+ const exchangeDetails = exchangeInfo.details;
+ if (!exchangeDetails) {
+ throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+ }
+ const currencyRecord = await oneShotGet(this.db,
Stores.currencies,
- exchangeInfo.currency,
- );
+ exchangeDetails.currency);
if (currencyRecord) {
for (const trustedExchange of currencyRecord.exchanges) {
- if (trustedExchange.exchangePub === exchangeInfo.masterPublicKey) {
+ if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
isTrusted = true;
break;
}
}
for (const trustedAuditor of currencyRecord.auditors) {
- for (const exchangeAuditor of exchangeInfo.auditors) {
+ for (const exchangeAuditor of exchangeDetails.auditors) {
if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
isAudited = true;
break;
@@ -1872,6 +1825,16 @@ export class Wallet {
amount: AmountJson,
): Promise<ReserveCreationInfo> {
const exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
+ const exchangeDetails = exchangeInfo.details;
+ if (!exchangeDetails) {
+ throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+ }
+ const exchangeWireInfo = exchangeInfo.wireInfo;
+ if (!exchangeWireInfo) {
+ throw Error(
+ `exchange ${exchangeInfo.baseUrl} wire details not available`,
+ );
+ }
const selectedDenoms = await this.getVerifiedWithdrawDenomList(
baseUrl,
@@ -1887,16 +1850,8 @@ export class Wallet {
)
.reduce((a, b) => Amounts.add(a, b).amount);
- const wireInfo = await this.getWireInfo(baseUrl);
-
- const wireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
- if (!wireFees) {
- // should never happen unless DB is inconsistent
- throw Error(`no wire fees found for exchange ${baseUrl}`);
- }
-
const exchangeWireAccounts: string[] = [];
- for (let account of wireInfo.accounts) {
+ for (let account of exchangeWireInfo.accounts) {
exchangeWireAccounts.push(account.url);
}
@@ -1910,17 +1865,14 @@ export class Wallet {
}
}
- const possibleDenoms =
- (await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
- .filter(d => d.isOffered)
- .toArray()) || [];
+ const possibleDenoms = await oneShotIterIndex(
+ this.db,
+ Stores.denominations.exchangeBaseUrlIndex,
+ baseUrl)
+ .filter((d) => d.isOffered);
const trustedAuditorPubs = [];
- const currencyRecord = await this.q().get<CurrencyRecord>(
- Stores.currencies,
- amount.currency,
- );
+ const currencyRecord = await oneShotGet(this.db, Stores.currencies, amount.currency);
if (currencyRecord) {
trustedAuditorPubs.push(
...currencyRecord.auditors.map(a => a.auditorPub),
@@ -1928,10 +1880,10 @@ export class Wallet {
}
let versionMatch;
- if (exchangeInfo.protocolVersion) {
+ if (exchangeDetails.protocolVersion) {
versionMatch = LibtoolVersion.compare(
WALLET_PROTOCOL_VERSION,
- exchangeInfo.protocolVersion,
+ exchangeDetails.protocolVersion,
);
if (
@@ -1940,10 +1892,10 @@ export class Wallet {
versionMatch.currentCmp === -1
) {
console.warn(
- `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated (exchange has ${exchangeInfo.protocolVersion}), checking for updates`,
+ `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated (exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
);
if (isFirefox()) {
- console.log("skipping update check on Firefox")
+ console.log("skipping update check on Firefox");
} else {
chrome.runtime.requestUpdateCheck((status, details) => {
console.log("update check status:", status);
@@ -1956,7 +1908,7 @@ export class Wallet {
earliestDepositExpiration,
exchangeInfo,
exchangeWireAccounts,
- exchangeVersion: exchangeInfo.protocolVersion || "unknown",
+ exchangeVersion: exchangeDetails.protocolVersion || "unknown",
isAudited,
isTrusted,
numOfferedDenoms: possibleDenoms.length,
@@ -1965,7 +1917,7 @@ export class Wallet {
trustedAuditorPubs,
versionMatch,
walletVersion: WALLET_PROTOCOL_VERSION,
- wireFees,
+ wireFees: exchangeWireInfo,
withdrawFee: acc,
};
return ret;
@@ -1975,8 +1927,15 @@ export class Wallet {
exchangeBaseUrl: string,
supportedTargetTypes: string[],
): Promise<string> {
- const wireInfo = await this.getWireInfo(exchangeBaseUrl);
- for (let account of wireInfo.accounts) {
+ const exchangeRecord = await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
+ if (!exchangeRecord) {
+ throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
+ }
+ const exchangeWireInfo = exchangeRecord.wireInfo;
+ if (!exchangeWireInfo) {
+ throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
+ }
+ for (let account of exchangeWireInfo.accounts) {
const paytoUri = new URI(account.url);
if (supportedTargetTypes.includes(paytoUri.authority())) {
return account.url;
@@ -1986,235 +1945,173 @@ export class Wallet {
}
/**
- * Update or add exchange DB entry by fetching the /keys information.
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
* Optionally link the reserve entry to the new or existing
* exchange entry in then DB.
*/
- async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
- baseUrl = canonicalizeBaseUrl(baseUrl);
- const keysUrl = new URI("keys")
- .absoluteTo(baseUrl)
- .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
- const keysResp = await this.http.get(keysUrl.href());
- if (keysResp.status !== 200) {
- throw Error("/keys request failed");
- }
- const exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
- const exchangeWire = await this.getWireInfo(baseUrl);
- return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, exchangeWire);
- }
-
- private async suspendCoins(exchangeInfo: ExchangeRecord): Promise<void> {
- const resultSuspendedCoins = await this.q()
- .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl)
- .indexJoinLeft(
- Stores.denominations.exchangeBaseUrlIndex,
- e => e.exchangeBaseUrl,
- )
- .fold(
- (
- cd: JoinLeftResult<CoinRecord, DenominationRecord>,
- suspendedCoins: CoinRecord[],
- ) => {
- if (!cd.right || !cd.right.isOffered) {
- return Array.prototype.concat(suspendedCoins, [cd.left]);
- }
- return Array.prototype.concat(suspendedCoins);
- },
- [],
- );
-
- const q = this.q();
- resultSuspendedCoins.map((c: CoinRecord) => {
- Wallet.enableTracing && console.log("suspending coin", c);
- c.suspended = true;
- q.put(Stores.coins, c);
- this.badge.showNotification();
- this.notifier.notify();
- });
- await q.finish();
- }
-
- private async updateExchangeFromJson(
+ async updateExchangeFromUrl(
baseUrl: string,
- exchangeKeysJson: KeysJson,
- wireMethodDetails: ExchangeWireJson,
+ force: boolean = false,
): Promise<ExchangeRecord> {
- // FIXME: all this should probably be commited atomically
- const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
- if (updateTimeSec === null) {
- throw Error("invalid update time");
- }
-
- if (exchangeKeysJson.denoms.length === 0) {
- throw Error("exchange doesn't offer any denominations");
- }
-
- const r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
-
- let exchangeInfo: ExchangeRecord;
+ const now = getTimestampNow();
+ baseUrl = canonicalizeBaseUrl(baseUrl);
+ const r = await oneShotGet(this.db, Stores.exchanges, baseUrl);
if (!r) {
- exchangeInfo = {
- auditors: exchangeKeysJson.auditors,
- baseUrl,
- currency: Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
- .currency,
- lastUpdateTime: updateTimeSec,
- lastUsedTime: 0,
- masterPublicKey: exchangeKeysJson.master_public_key,
+ const newExchangeRecord: ExchangeRecord = {
+ baseUrl: baseUrl,
+ details: undefined,
+ wireInfo: undefined,
+ updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
+ updateStarted: now,
};
- Wallet.enableTracing && console.log("making fresh exchange");
+ await oneShotPut(this.db, Stores.exchanges, newExchangeRecord);
} else {
- if (updateTimeSec < r.lastUpdateTime) {
- Wallet.enableTracing && console.log("outdated /keys, not updating");
- return r;
- }
- exchangeInfo = r;
- exchangeInfo.lastUpdateTime = updateTimeSec;
- Wallet.enableTracing && console.log("updating old exchange");
- }
-
- const updatedExchangeInfo = await this.updateExchangeInfo(
- exchangeInfo,
- exchangeKeysJson,
- );
- await this.suspendCoins(updatedExchangeInfo);
- updatedExchangeInfo.protocolVersion = exchangeKeysJson.version;
-
- await this.q()
- .put(Stores.exchanges, updatedExchangeInfo)
- .finish();
-
- let oldWireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
- if (!oldWireFees) {
- oldWireFees = {
- exchangeBaseUrl: baseUrl,
- feesForType: {},
- };
- }
-
- for (const paytoTargetType in wireMethodDetails.fees) {
- let latestFeeStamp = 0;
- const newFeeDetails = wireMethodDetails.fees[paytoTargetType];
- const oldFeeDetails = oldWireFees.feesForType[paytoTargetType] || [];
- oldWireFees.feesForType[paytoTargetType] = oldFeeDetails;
- for (const oldFee of oldFeeDetails) {
- if (oldFee.endStamp > latestFeeStamp) {
- latestFeeStamp = oldFee.endStamp;
- }
- }
- for (const fee of newFeeDetails) {
- const start = getTalerStampSec(fee.start_date);
- if (start === null) {
- console.error("invalid start stamp in fee", fee);
- continue;
- }
- if (start < latestFeeStamp) {
- continue;
- }
- const end = getTalerStampSec(fee.end_date);
- if (end === null) {
- console.error("invalid end stamp in fee", fee);
- continue;
+ runWithWriteTransaction(this.db, [Stores.exchanges], async (t) => {
+ const rec = await t.get(Stores.exchanges, baseUrl);
+ if (!rec) {
+ return;
}
- const wf: WireFee = {
- closingFee: Amounts.parseOrThrow(fee.closing_fee),
- endStamp: end,
- sig: fee.sig,
- startStamp: start,
- wireFee: Amounts.parseOrThrow(fee.wire_fee),
- };
- const valid: boolean = await this.cryptoApi.isValidWireFee(
- paytoTargetType,
- wf,
- exchangeInfo.masterPublicKey,
- );
- if (!valid) {
- console.error("fee signature invalid", fee);
- throw Error("fee signature invalid");
+ if (rec.updateStatus != ExchangeUpdateStatus.NONE && !force) {
+ return;
}
- oldFeeDetails.push(wf);
- }
+ rec.updateStarted = now;
+ rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
+ t.put(Stores.exchanges, rec);
+ });
}
- await this.q().put(Stores.exchangeWireFees, oldWireFees);
+ await this.updateExchangeWithKeys(baseUrl);
+ await this.updateExchangeWithWireInfo(baseUrl);
- if (exchangeKeysJson.payback) {
- for (const payback of exchangeKeysJson.payback) {
- const denom = await this.q().getIndexed(
- Stores.denominations.denomPubHashIndex,
- payback.h_denom_pub,
- );
- if (!denom) {
- continue;
- }
- Wallet.enableTracing && console.log(`cashing back denom`, denom);
- const coins = await this.q()
- .iterIndex(Stores.coins.denomPubIndex, denom.denomPub)
- .toArray();
- for (const coin of coins) {
- this.payback(coin.coinPub);
- }
- }
+ const updatedExchange = await oneShotGet(this.db, Stores.exchanges, baseUrl);
+
+ if (!updatedExchange) {
+ // This should practically never happen
+ throw Error("exchange not found");
}
+ return updatedExchange;
+ }
- return updatedExchangeInfo;
+ private async setExchangeError(
+ baseUrl: string,
+ err: OperationError,
+ ): Promise<void> {
+ const mut = (exchange: ExchangeRecord) => {
+ exchange.lastError = err;
+ return exchange;
+ };
+ await oneShotMutate(this.db, Stores.exchanges, baseUrl, mut);
}
- private async updateExchangeInfo(
- exchangeInfo: ExchangeRecord,
- newKeys: KeysJson,
- ): Promise<ExchangeRecord> {
- if (exchangeInfo.masterPublicKey !== newKeys.master_public_key) {
- throw Error("public keys do not match");
+ /**
+ * Fetch the exchange's /keys and update our database accordingly.
+ *
+ * Exceptions thrown in this method must be caught and reported
+ * in the pending operations.
+ */
+ private async updateExchangeWithKeys(baseUrl: string): Promise<void> {
+ const existingExchangeRecord = await oneShotGet(this.db, Stores.exchanges, baseUrl);
+
+ if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
+ return;
+ }
+ const keysUrl = new URI("keys")
+ .absoluteTo(baseUrl)
+ .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+ let keysResp;
+ try {
+ keysResp = await this.http.get(keysUrl.href());
+ } catch (e) {
+ await this.setExchangeError(baseUrl, {
+ type: "network",
+ details: {},
+ message: `Fetching keys failed: ${e.message}`,
+ });
+ throw e;
+ }
+ let exchangeKeysJson: KeysJson;
+ try {
+ exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
+ } catch (e) {
+ await this.setExchangeError(baseUrl, {
+ type: "protocol-violation",
+ details: {},
+ message: `Parsing /keys response failed: ${e.message}`,
+ });
+ throw e;
}
- const existingDenoms: {
- [denomPub: string]: DenominationRecord;
- } = await this.q()
- .iterIndex(
- Stores.denominations.exchangeBaseUrlIndex,
- exchangeInfo.baseUrl,
- )
- .fold(
- (x: DenominationRecord, acc: typeof existingDenoms) => (
- (acc[x.denomPub] = x), acc
- ),
- {},
- );
+ const lastUpdateTimestamp = extractTalerStamp(
+ exchangeKeysJson.list_issue_date,
+ );
+ if (!lastUpdateTimestamp) {
+ const m = `Parsing /keys response failed: invalid list_issue_date.`;
+ await this.setExchangeError(baseUrl, {
+ type: "protocol-violation",
+ details: {},
+ message: m,
+ });
+ throw Error(m);
+ }
- const newDenoms: typeof existingDenoms = {};
- const newAndUnseenDenoms: typeof existingDenoms = {};
+ if (exchangeKeysJson.denoms.length === 0) {
+ const m = "exchange doesn't offer any denominations";
+ await this.setExchangeError(baseUrl, {
+ type: "protocol-violation",
+ details: {},
+ message: m,
+ });
+ throw Error(m);
+ }
- for (const d of newKeys.denoms) {
- const dr = await this.denominationRecordFromKeys(exchangeInfo.baseUrl, d);
- if (!(d.denom_pub in existingDenoms)) {
- newAndUnseenDenoms[dr.denomPub] = dr;
- }
- newDenoms[dr.denomPub] = dr;
+ const protocolVersion = exchangeKeysJson.version;
+ if (!protocolVersion) {
+ const m = "outdate exchange, no version in /keys response";
+ await this.setExchangeError(baseUrl, {
+ type: "protocol-violation",
+ details: {},
+ message: m,
+ });
+ throw Error(m);
}
- for (const oldDenomPub in existingDenoms) {
- if (!(oldDenomPub in newDenoms)) {
- const d = existingDenoms[oldDenomPub];
- d.isOffered = false;
+ const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
+ .currency;
+
+ const mutExchangeRecord = (r: ExchangeRecord) => {
+ if (r.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
+ console.log("not updating, wrong state (concurrent modification?)");
+ return undefined;
}
- }
+ r.details = {
+ currency,
+ protocolVersion,
+ lastUpdateTime: lastUpdateTimestamp,
+ masterPublicKey: exchangeKeysJson.master_public_key,
+ auditors: exchangeKeysJson.auditors,
+ };
+ r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
+ r.lastError = undefined;
+ return r;
+ };
+ }
- await this.q()
- .putAll(
- Stores.denominations,
- Object.keys(newAndUnseenDenoms).map(d => newAndUnseenDenoms[d]),
- )
- .putAll(
- Stores.denominations,
- Object.keys(existingDenoms).map(d => existingDenoms[d]),
- )
- .finish();
- return exchangeInfo;
+ private async updateExchangeWithWireInfo(exchangeBaseUrl: string) {
+ exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+ const reqUrl = new URI("wire")
+ .absoluteTo(exchangeBaseUrl)
+ .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+ const resp = await this.http.get(reqUrl.href());
+
+ const wiJson = resp.responseJson;
+ if (!wiJson) {
+ throw Error("/wire response malformed");
+ }
+ const wireInfo = ExchangeWireJson.checked(wiJson);
}
+
/**
* Get detailed balance information, sliced by exchange and by currency.
*/
@@ -2253,127 +2150,85 @@ export class Wallet {
entryEx[field] = Amounts.add(entryEx[field], amount).amount;
}
- function collectBalances(c: CoinRecord, balance: WalletBalance) {
- if (c.suspended) {
- return balance;
- }
- if (c.status === CoinStatus.Fresh) {
- addTo(balance, "available", c.currentAmount, c.exchangeBaseUrl);
- return balance;
- }
- if (c.status === CoinStatus.Dirty) {
- addTo(balance, "pendingIncoming", c.currentAmount, c.exchangeBaseUrl);
- addTo(balance, "pendingIncomingDirty", c.currentAmount, c.exchangeBaseUrl);
- return balance;
- }
- return balance;
- }
-
- function collectPendingWithdraw(r: ReserveRecord, balance: WalletBalance) {
- if (!r.timestamp_confirmed) {
- return balance;
- }
- let amount = Amounts.getZero(r.requested_amount.currency);
- /*
- let amount = r.current_amount;
- if (!amount) {
- amount = r.requested_amount;
- }
- */
- amount = Amounts.add(amount, r.precoin_amount).amount;
- if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], amount) < 0) {
- addTo(balance, "pendingIncoming", amount, r.exchange_base_url);
- addTo(balance, "pendingIncomingWithdraw", amount, r.exchange_base_url);
- }
- return balance;
- }
-
- function collectPaybacks(r: ReserveRecord, balance: WalletBalance) {
- if (!r.hasPayback) {
- return balance;
- }
- if (
- Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) <
- 0
- ) {
- addTo(balance, "paybackAmount", r.current_amount!, r.exchange_base_url);
- }
- return balance;
- }
+ const balanceStore = {
+ byCurrency: {},
+ byExchange: {},
+ };
- function collectPendingRefresh(
- r: RefreshSessionRecord,
- balance: WalletBalance,
- ) {
+ await runWithWriteTransaction(this.db, [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases], async (tx) => {
+ await tx.iter(Stores.coins).forEach((c) => {
+ if (c.suspended) {
+ return;
+ }
+ if (c.status === CoinStatus.Fresh) {
+ addTo(balanceStore, "available", c.currentAmount, c.exchangeBaseUrl);
+ }
+ if (c.status === CoinStatus.Dirty) {
+ addTo(balanceStore, "pendingIncoming", c.currentAmount, c.exchangeBaseUrl);
+ addTo(
+ balanceStore,
+ "pendingIncomingDirty",
+ c.currentAmount,
+ c.exchangeBaseUrl,
+ );
+ }
+ });
+ await tx.iter(Stores.refresh).forEach((r) => {
// Don't count finished refreshes, since the refresh already resulted
// in coins being added to the wallet.
- if (r.finished) {
- return balance;
- }
- addTo(balance, "pendingIncoming", r.valueOutput, r.exchangeBaseUrl);
- addTo(balance, "pendingIncomingRefresh", r.valueOutput, r.exchangeBaseUrl);
-
- return balance;
- }
-
- function collectPayments(t: PurchaseRecord, balance: WalletBalance) {
- if (t.finished) {
- return balance;
- }
- for (const c of t.payReq.coins) {
+ if (r.finished) {
+ return;
+ }
+ addTo(balanceStore, "pendingIncoming", r.valueOutput, r.exchangeBaseUrl);
addTo(
- balance,
- "pendingPayment",
- Amounts.parseOrThrow(c.contribution),
- c.exchange_url,
+ balanceStore,
+ "pendingIncomingRefresh",
+ r.valueOutput,
+ r.exchangeBaseUrl,
);
- }
- return balance;
- }
+ });
- function collectSmallestWithdraw(
- e: JoinResult<ExchangeRecord, DenominationRecord>,
- sw: any,
- ) {
- let min = sw[e.left.baseUrl];
- const v = Amounts.add(e.right.value, e.right.feeWithdraw).amount;
- if (!min) {
- min = v;
- } else if (Amounts.cmp(v, min) < 0) {
- min = v;
- }
- sw[e.left.baseUrl] = min;
- return sw;
- }
+ await tx.iter(Stores.reserves).forEach((r) => {
+ if (!r.timestamp_confirmed) {
+ return;
+ }
+ let amount = Amounts.getZero(r.requested_amount.currency);
+ amount = Amounts.add(amount, r.precoin_amount).amount;
+ addTo(balanceStore, "pendingIncoming", amount, r.exchange_base_url);
+ addTo(balanceStore, "pendingIncomingWithdraw", amount, r.exchange_base_url);
+ });
- const balanceStore = {
- byCurrency: {},
- byExchange: {},
- };
- // Mapping from exchange pub to smallest
- // possible amount we can withdraw
- let smallestWithdraw: { [baseUrl: string]: AmountJson } = {};
-
- smallestWithdraw = await this.q()
- .iter(Stores.exchanges)
- .indexJoin(Stores.denominations.exchangeBaseUrlIndex, x => x.baseUrl)
- .fold(collectSmallestWithdraw, {});
-
- const tx = this.q();
- tx.iter(Stores.coins).fold(collectBalances, balanceStore);
- tx.iter(Stores.refresh).fold(collectPendingRefresh, balanceStore);
- tx.iter(Stores.reserves).fold(collectPendingWithdraw, balanceStore);
- tx.iter(Stores.reserves).fold(collectPaybacks, balanceStore);
- tx.iter(Stores.purchases).fold(collectPayments, balanceStore);
- await tx.finish();
- Wallet.enableTracing && console.log("computed balances:", balanceStore)
+ await tx.iter(Stores.reserves).forEach((r) => {
+ if (!r.hasPayback) {
+ return;
+ }
+ addTo(balanceStore, "paybackAmount", r.current_amount!, r.exchange_base_url);
+ return balanceStore;
+ });
+
+ await tx.iter(Stores.purchases).forEach((t) => {
+ if (t.finished) {
+ return;
+ }
+ for (const c of t.payReq.coins) {
+ addTo(
+ balanceStore,
+ "pendingPayment",
+ Amounts.parseOrThrow(c.contribution),
+ c.exchange_url,
+ );
+ }
+ });
+ });
+
+ Wallet.enableTracing && console.log("computed balances:", balanceStore);
return balanceStore;
}
async createRefreshSession(
oldCoinPub: string,
): Promise<RefreshSessionRecord | undefined> {
- const coin = await this.q().get<CoinRecord>(Stores.coins, oldCoinPub);
+ const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
if (!coin) {
throw Error("coin not found");
@@ -2389,7 +2244,7 @@ export class Wallet {
throw Error("db inconsistent");
}
- const oldDenom = await this.q().get(Stores.denominations, [
+ const oldDenom = await oneShotGet(this.db, Stores.denominations, [
exchange.baseUrl,
coin.denomPub,
]);
@@ -2398,9 +2253,7 @@ export class Wallet {
throw Error("db inconsistent");
}
- const availableDenoms: DenominationRecord[] = await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
- .toArray();
+ const availableDenoms: DenominationRecord[] = await oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
.amount;
@@ -2421,7 +2274,7 @@ export class Wallet {
)} too small`,
);
coin.status = CoinStatus.Useless;
- await this.q().put(Stores.coins, coin);
+ await oneShotPut(this.db, Stores.coins, coin);
this.notifier.notify();
return undefined;
}
@@ -2445,16 +2298,16 @@ export class Wallet {
return c;
}
+ let key;
+
// Store refresh session and subtract refreshed amount from
// coin in the same transaction.
- const query = this.q();
- query
- .put(Stores.refresh, refreshSession, "refreshKey")
- .mutate(Stores.coins, coin.coinPub, mutateCoin);
- await query.finish();
+ await runWithWriteTransaction(this.db, [Stores.refresh, Stores.coins], async (tx) => {
+ key = await tx.put(Stores.refresh, refreshSession);
+ await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
+ });
this.notifier.notify();
- const key = query.key("refreshKey");
if (!key || typeof key !== "number") {
throw Error("insert failed");
}
@@ -2466,18 +2319,20 @@ export class Wallet {
async refresh(oldCoinPub: string): Promise<void> {
const refreshImpl = async () => {
- const oldRefreshSessions = await this.q()
- .iter(Stores.refresh)
- .toArray();
+ const oldRefreshSessions = await oneShotIter(this.db, Stores.refresh).toArray();
for (const session of oldRefreshSessions) {
if (session.finished) {
continue;
}
Wallet.enableTracing &&
- console.log("waiting for unfinished old refresh session for", oldCoinPub, session);
+ console.log(
+ "waiting for unfinished old refresh session for",
+ oldCoinPub,
+ session,
+ );
await this.continueRefreshSession(session);
}
- const coin = await this.q().get(Stores.coins, oldCoinPub);
+ const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
if (!coin) {
console.warn("can't refresh, coin not in database");
return;
@@ -2486,7 +2341,11 @@ export class Wallet {
coin.status === CoinStatus.Useless ||
coin.status === CoinStatus.Fresh
) {
- Wallet.enableTracing && console.log("not refreshing due to coin status", CoinStatus[coin.status])
+ Wallet.enableTracing &&
+ console.log(
+ "not refreshing due to coin status",
+ CoinStatus[coin.status],
+ );
return;
}
const refreshSession = await this.createRefreshSession(oldCoinPub);
@@ -2520,10 +2379,7 @@ export class Wallet {
}
if (typeof refreshSession.norevealIndex !== "number") {
await this.refreshMelt(refreshSession);
- const r = await this.q().get<RefreshSessionRecord>(
- Stores.refresh,
- refreshSession.id,
- );
+ const r = await oneShotGet(this.db, Stores.refresh, refreshSession.id);
if (!r) {
throw Error("refresh session does not exist anymore");
}
@@ -2539,10 +2395,8 @@ export class Wallet {
return;
}
- const coin = await this.q().get<CoinRecord>(
- Stores.coins,
- refreshSession.meltCoinPub,
- );
+ const coin = await oneShotGet(this.db, Stores.coins, refreshSession.meltCoinPub);
+
if (!coin) {
console.error("can't melt coin, it does not exist");
return;
@@ -2579,9 +2433,8 @@ export class Wallet {
refreshSession.norevealIndex = norevealIndex;
- await this.q()
- .put(Stores.refresh, refreshSession)
- .finish();
+ await oneShotPut(this.db, Stores.refresh, refreshSession);
+
this.notifier.notify();
}
@@ -2598,10 +2451,7 @@ export class Wallet {
throw Error("refresh index error");
}
- const meltCoinRecord = await this.q().get(
- Stores.coins,
- refreshSession.meltCoinPub,
- );
+ const meltCoinRecord = await oneShotGet(this.db, Stores.coins, refreshSession.meltCoinPub);
if (!meltCoinRecord) {
throw Error("inconsistent database");
}
@@ -2657,10 +2507,7 @@ export class Wallet {
return;
}
- const exchange = await this.q().get<ExchangeRecord>(
- Stores.exchanges,
- refreshSession.exchangeBaseUrl,
- );
+ const exchange = await this.findExchange(refreshSession.exchangeBaseUrl);
if (!exchange) {
console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`);
return;
@@ -2669,7 +2516,7 @@ export class Wallet {
const coins: CoinRecord[] = [];
for (let i = 0; i < respJson.ev_sigs.length; i++) {
- const denom = await this.q().get(Stores.denominations, [
+ const denom = await oneShotGet(this.db, Stores.denominations, [
refreshSession.exchangeBaseUrl,
refreshSession.newDenoms[i],
]);
@@ -2702,17 +2549,25 @@ export class Wallet {
refreshSession.finished = true;
- await this.q()
- .putAll(Stores.coins, coins)
- .put(Stores.refresh, refreshSession)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.coins, Stores.refresh], async (tx) => {
+ for (let coin of coins) {
+ await tx.put(Stores.coins, coin);
+ }
+ await tx.put(Stores.refresh, refreshSession);
+ });
this.notifier.notify();
}
+ async findExchange(exchangeBaseUrl: string): Promise<ExchangeRecord | undefined> {
+ return await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
+ }
+
/**
* Retrive the full event history for this wallet.
*/
- async getHistory(historyQuery?: HistoryQuery): Promise<{ history: HistoryRecord[] }> {
+ async getHistory(
+ historyQuery?: HistoryQuery,
+ ): Promise<{ history: HistoryRecord[] }> {
const history: HistoryRecord[] = [];
// FIXME: do pagination instead of generating the full history
@@ -2721,9 +2576,7 @@ export class Wallet {
// This works as timestamps are guaranteed to be monotonically
// increasing even
- const proposals = await this.q()
- .iter<ProposalDownloadRecord>(Stores.proposals)
- .toArray();
+ const proposals = await oneShotIter(this.db, Stores.proposals).toArray();
for (const p of proposals) {
history.push({
detail: {
@@ -2735,7 +2588,7 @@ export class Wallet {
});
}
- const withdrawals = await this.q().iter<WithdrawalRecord>(Stores.withdrawals).toArray()
+ const withdrawals = await oneShotIter(this.db, Stores.withdrawals).toArray();
for (const w of withdrawals) {
history.push({
detail: {
@@ -2746,9 +2599,7 @@ export class Wallet {
});
}
- const purchases = await this.q()
- .iter<PurchaseRecord>(Stores.purchases)
- .toArray();
+ const purchases = await oneShotIter(this.db, Stores.purchases).toArray();
for (const p of purchases) {
history.push({
detail: {
@@ -2787,9 +2638,8 @@ export class Wallet {
}
}
- const reserves: ReserveRecord[] = await this.q()
- .iter<ReserveRecord>(Stores.reserves)
- .toArray();
+ const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
+
for (const r of reserves) {
history.push({
detail: {
@@ -2813,9 +2663,7 @@ export class Wallet {
}
}
- const tips: TipRecord[] = await this.q()
- .iter<TipRecord>(Stores.tips)
- .toArray();
+ const tips: TipRecord[] = await oneShotIter(this.db, Stores.tips).toArray();
for (const tip of tips) {
history.push({
detail: {
@@ -2835,78 +2683,87 @@ export class Wallet {
}
async getPendingOperations(): Promise<PendingOperationsResponse> {
+ const pendingOperations: PendingOperationInfo[] = [];
+ const exchanges = await this.getExchanges();
+ for (let e of exchanges) {
+ switch (e.updateStatus) {
+ case ExchangeUpdateStatus.NONE:
+ if (!e.details) {
+ pendingOperations.push({
+ type: "bug",
+ message:
+ "Exchange record does not have details, but no update in progress.",
+ details: {
+ exchangeBaseUrl: e.baseUrl,
+ },
+ });
+ }
+ break;
+ case ExchangeUpdateStatus.FETCH_KEYS:
+ pendingOperations.push({
+ type: "exchange-update",
+ stage: "fetch-keys",
+ exchangeBaseUrl: e.baseUrl,
+ });
+ break;
+ case ExchangeUpdateStatus.FETCH_WIRE:
+ pendingOperations.push({
+ type: "exchange-update",
+ stage: "fetch-wire",
+ exchangeBaseUrl: e.baseUrl,
+ });
+ break;
+ }
+ }
return {
- pendingOperations: []
+ pendingOperations,
};
}
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
- const denoms = await this.q()
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
- .toArray();
+ const denoms = await oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex, exchangeUrl).toArray();
return denoms;
}
async getProposal(
proposalId: number,
): Promise<ProposalDownloadRecord | undefined> {
- const proposal = await this.q().get(Stores.proposals, proposalId);
+ const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
return proposal;
}
async getExchanges(): Promise<ExchangeRecord[]> {
- return this.q()
- .iter<ExchangeRecord>(Stores.exchanges)
- .toArray();
+ return await oneShotIter(this.db, Stores.exchanges).toArray();
}
async getCurrencies(): Promise<CurrencyRecord[]> {
- return this.q()
- .iter<CurrencyRecord>(Stores.currencies)
- .toArray();
+ return await oneShotIter(this.db, Stores.currencies).toArray();
}
async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
Wallet.enableTracing && console.log("updating currency to", currencyRecord);
- await this.q()
- .put(Stores.currencies, currencyRecord)
- .finish();
+ await oneShotPut(this.db, Stores.currencies, currencyRecord);
this.notifier.notify();
}
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
- return this.q()
- .iter<ReserveRecord>(Stores.reserves)
- .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl)
- .toArray();
+ return await oneShotIter(this.db, Stores.reserves).filter((r) => r.exchange_base_url === exchangeBaseUrl);
}
async getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
- return this.q()
- .iter<CoinRecord>(Stores.coins)
- .filter((c: CoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl)
- .toArray();
+ return await oneShotIter(this.db, Stores.coins).filter((c) => c.exchangeBaseUrl === exchangeBaseUrl);
}
async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
- return this.q()
- .iter<PreCoinRecord>(Stores.precoins)
- .filter((c: PreCoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl)
- .toArray();
+ return await oneShotIter(this.db, Stores.precoins).filter((c) => c.exchangeBaseUrl === exchangeBaseUrl);
}
- async hashContract(contract: ContractTerms): Promise<string> {
+ private async hashContract(contract: ContractTerms): Promise<string> {
return this.cryptoApi.hashString(canonicalJson(contract));
}
- async getCurrencyRecord(
- currency: string,
- ): Promise<CurrencyRecord | undefined> {
- return this.q().get(Stores.currencies, currency);
- }
-
async payback(coinPub: string): Promise<void> {
- let coin = await this.q().get(Stores.coins, coinPub);
+ let coin = await oneShotGet(this.db, Stores.coins, coinPub);
if (!coin) {
throw Error(`Coin ${coinPub} not found, can't request payback`);
}
@@ -2914,7 +2771,7 @@ export class Wallet {
if (!reservePub) {
throw Error(`Can't request payback for a refreshed coin`);
}
- const reserve = await this.q().get(Stores.reserves, reservePub);
+ const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
if (!reserve) {
throw Error(`Reserve of coin ${coinPub} not found`);
}
@@ -2932,9 +2789,10 @@ export class Wallet {
// technically we might update reserve status before we get the response
// from the reserve for the payback request.
reserve.hasPayback = true;
- await this.q()
- .put(Stores.coins, coin)
- .put(Stores.reserves, reserve);
+ await runWithWriteTransaction(this.db, [Stores.coins, Stores.reserves], async (tx) => {
+ await tx.put(Stores.coins, coin!!);
+ await tx.put(Stores.reserves, reserve);
+ });
this.notifier.notify();
const paybackRequest = await this.cryptoApi.createPaybackRequest(coin);
@@ -2947,17 +2805,17 @@ export class Wallet {
if (paybackConfirmation.reserve_pub !== coin.reservePub) {
throw Error(`Coin's reserve doesn't match reserve on payback`);
}
- coin = await this.q().get(Stores.coins, coinPub);
+ coin = await oneShotGet(this.db, Stores.coins, coinPub);
if (!coin) {
throw Error(`Coin ${coinPub} not found, can't confirm payback`);
}
coin.status = CoinStatus.PaybackDone;
- await this.q().put(Stores.coins, coin);
+ await oneShotPut(this.db, Stores.coins, coin);
this.notifier.notify();
await this.updateReserve(reservePub!);
}
- async denominationRecordFromKeys(
+ private async denominationRecordFromKeys(
exchangeBaseUrl: string,
denomIn: Denomination,
): Promise<DenominationRecord> {
@@ -2983,20 +2841,17 @@ export class Wallet {
}
async withdrawPaybackReserve(reservePub: string): Promise<void> {
- const reserve = await this.q().get(Stores.reserves, reservePub);
+ const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
if (!reserve) {
throw Error(`Reserve ${reservePub} does not exist`);
}
reserve.hasPayback = false;
- await this.q().put(Stores.reserves, reserve);
+ await oneShotPut(this.db, Stores.reserves, reserve);
this.depleteReserve(reserve);
}
async getPaybackReserves(): Promise<ReserveRecord[]> {
- return await this.q()
- .iter(Stores.reserves)
- .filter(r => r.hasPayback)
- .toArray();
+ return await oneShotIter(this.db, Stores.reserves).filter(r => r.hasPayback);
}
/**
@@ -3009,13 +2864,16 @@ export class Wallet {
async getSenderWireInfos(): Promise<SenderWireInfos> {
const m: { [url: string]: Set<string> } = {};
- await this.q()
- .iter(Stores.exchangeWireFees)
- .map(x => {
- const s = (m[x.exchangeBaseUrl] = m[x.exchangeBaseUrl] || new Set());
- Object.keys(x.feesForType).map(k => s.add(k));
- })
- .run();
+
+ await oneShotIter(this.db, Stores.exchanges).forEach((x) => {
+ const wi = x.wireInfo;
+ if (!wi) {
+ return;
+ }
+ const s = (m[x.baseUrl] = m[x.baseUrl] || new Set());
+ Object.keys(wi.feesForType).map(k => s.add(k));
+ });
+
Wallet.enableTracing && console.log(m);
const exchangeWireTypes: { [url: string]: string[] } = {};
Object.keys(m).map(e => {
@@ -3023,12 +2881,10 @@ export class Wallet {
});
const senderWiresSet: Set<string> = new Set();
- await this.q()
- .iter(Stores.senderWires)
- .map(x => {
- senderWiresSet.add(x.paytoUri);
- })
- .run();
+ await oneShotIter(this.db, Stores.senderWires).forEach((x) => {
+ senderWiresSet.add(x.paytoUri);
+ });
+
const senderWires: string[] = Array.from(senderWiresSet);
return {
@@ -3049,11 +2905,15 @@ export class Wallet {
return;
}
const stampSecNow = Math.floor(new Date().getTime() / 1000);
- const exchange = await this.q().get(Stores.exchanges, req.exchange);
+ const exchange = await this.findExchange(req.exchange);
if (!exchange) {
console.error(`Exchange ${req.exchange} not known to the wallet`);
return;
}
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ throw Error("exchange information needs to be updated first.");
+ }
Wallet.enableTracing && console.log("selecting coins for return:", req);
const cds = await this.getCoinsForReturn(req.exchange, req.amount);
Wallet.enableTracing && console.log(cds);
@@ -3073,7 +2933,7 @@ export class Wallet {
amount: Amounts.toString(req.amount),
auditors: [],
exchanges: [
- { master_pub: exchange.masterPublicKey, url: exchange.baseUrl },
+ { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
],
extra: {},
fulfillment_url: "",
@@ -3114,10 +2974,12 @@ export class Wallet {
wire: req.senderWire,
};
- await this.q()
- .put(Stores.coinsReturns, coinsReturnRecord)
- .putAll(Stores.coins, payCoinInfo.updatedCoins)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.coinsReturns, Stores.coins], async (tx) => {
+ await tx.put(Stores.coinsReturns, coinsReturnRecord);
+ for (let c of payCoinInfo.updatedCoins) {
+ await tx.put(Stores.coins, c);
+ }
+ });
this.badge.showNotification();
this.notifier.notify();
@@ -3167,10 +3029,7 @@ export class Wallet {
// FIXME: verify signature
// For every successful deposit, we replace the old record with an updated one
- const currentCrr = await this.q().get(
- Stores.coinsReturns,
- coinsReturnRecord.contractTermsHash,
- );
+ const currentCrr = await oneShotGet(this.db, Stores.coinsReturns, coinsReturnRecord.contractTermsHash);
if (!currentCrr) {
console.error("database inconsistent");
continue;
@@ -3180,7 +3039,7 @@ export class Wallet {
nc.depositedSig = respJson.sig;
}
}
- await this.q().put(Stores.coinsReturns, currentCrr);
+ await oneShotPut(this.db, Stores.coinsReturns, currentCrr);
this.notifier.notify();
}
}
@@ -3220,9 +3079,7 @@ export class Wallet {
const hc = refundResponse.h_contract_terms;
// Add the refund permissions to the purchase within a DB transaction
- await this.q()
- .mutate(Stores.purchases, hc, f)
- .finish();
+ await oneShotMutate(this.db, Stores.purchases, hc, f);
this.notifier.notify();
await this.submitRefunds(hc);
@@ -3257,7 +3114,7 @@ export class Wallet {
}
private async submitRefunds(contractTermsHash: string): Promise<void> {
- const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+ const purchase = await oneShotGet(this.db, Stores.purchases, contractTermsHash);
if (!purchase) {
console.error(
"not submitting refunds, contract terms not found:",
@@ -3320,10 +3177,10 @@ export class Wallet {
return c;
};
- await this.q()
- .mutate(Stores.purchases, contractTermsHash, transformPurchase)
- .mutate(Stores.coins, perm.coin_pub, transformCoin)
- .finish();
+ await runWithWriteTransaction(this.db, [Stores.purchases, Stores.coins], async (tx) => {
+ await tx.mutate(Stores.purchases, contractTermsHash, transformPurchase);
+ await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
+ });
this.refresh(perm.coin_pub);
}
@@ -3334,7 +3191,7 @@ export class Wallet {
async getPurchase(
contractTermsHash: string,
): Promise<PurchaseRecord | undefined> {
- return this.q().get(Stores.purchases, contractTermsHash);
+ return oneShotGet(this.db, Stores.purchases, contractTermsHash);
}
async getFullRefundFees(
@@ -3343,10 +3200,7 @@ export class Wallet {
if (refundPermissions.length === 0) {
throw Error("no refunds given");
}
- const coin0 = await this.q().get(
- Stores.coins,
- refundPermissions[0].coin_pub,
- );
+ const coin0 = await oneShotGet(this.db, Stores.coins, refundPermissions[0].coin_pub);
if (!coin0) {
throw Error("coin not found");
}
@@ -3354,18 +3208,15 @@ export class Wallet {
Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
);
- const denoms = await this.q()
- .iterIndex(
- Stores.denominations.exchangeBaseUrlIndex,
- coin0.exchangeBaseUrl,
- )
- .toArray();
+ const denoms = await oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex,
+ coin0.exchangeBaseUrl).toArray()
+
for (const rp of refundPermissions) {
- const coin = await this.q().get(Stores.coins, rp.coin_pub);
+ const coin = await oneShotGet(this.db, Stores.coins, rp.coin_pub);
if (!coin) {
throw Error("coin not found");
}
- const denom = await this.q().get(Stores.denominations, [
+ const denom = await oneShotGet(this.db, Stores.denominations, [
coin0.exchangeBaseUrl,
coin.denomPub,
]);
@@ -3407,19 +3258,13 @@ export class Wallet {
tipId: string,
merchantOrigin: string,
): Promise<void> {
- let tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
+ let tipRecord = await oneShotGet(this.db, Stores.tips, [tipId, merchantOrigin]);
if (!tipRecord) {
throw Error("tip not in database");
}
tipRecord.accepted = true;
-
- // Create one transactional query, within this transaction
- // both the tip will be marked as accepted and coins
- // already withdrawn will be untainted.
- await this.q()
- .put(Stores.tips, tipRecord)
- .finish();
+ await oneShotPut(this.db, Stores.tips, tipRecord);
if (tipRecord.pickedUp) {
console.log("tip already picked up");
@@ -3437,7 +3282,7 @@ export class Wallet {
);
const coinPubs: string[] = planchets.map(x => x.coinPub);
- await this.q().mutate(Stores.tips, [tipId, merchantOrigin], r => {
+ await oneShotMutate(this.db, Stores.tips, [tipId, merchantOrigin], (r) => {
if (!r.planchets) {
r.planchets = planchets;
r.coinPubs = coinPubs;
@@ -3448,7 +3293,7 @@ export class Wallet {
this.notifier.notify();
}
- tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
+ tipRecord = await oneShotGet(this.db, Stores.tips, [tipId, merchantOrigin]);
if (!tipRecord) {
throw Error("tip not in database");
}
@@ -3497,15 +3342,14 @@ export class Wallet {
reservePub: response.reserve_pub,
withdrawSig: response.reserve_sigs[i].reserve_sig,
};
- await this.q().put(Stores.precoins, preCoin);
+ await oneShotPut(this.db, Stores.precoins, preCoin);
await this.processPreCoin(preCoin.coinPub);
}
tipRecord.pickedUp = true;
- await this.q()
- .put(Stores.tips, tipRecord)
- .finish();
+ await oneShotPut(this.db, Stores.tips, tipRecord);
+
this.notifier.notify();
this.badge.showNotification();
return;
@@ -3529,10 +3373,11 @@ export class Wallet {
let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
- let tipRecord = await this.q().get(Stores.tips, [
+ let tipRecord = await oneShotGet(this.db, Stores.tips, [
res.tipId,
res.merchantOrigin,
- ]);
+ ])
+
if (!tipRecord) {
const withdrawDetails = await this.getWithdrawDetailsForAmount(
tipPickupStatus.exchange_url,
@@ -3558,7 +3403,7 @@ export class Wallet {
withdrawDetails.withdrawFee,
).amount,
};
- await this.q().put(Stores.tips, tipRecord);
+ await oneShotPut(this.db, Stores.tips, tipRecord);
}
const tipStatus: TipStatus = {
@@ -3578,7 +3423,7 @@ export class Wallet {
}
async abortFailedPayment(contractTermsHash: string): Promise<void> {
- const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+ const purchase = await oneShotGet(this.db, Stores.purchases, contractTermsHash);
if (!purchase) {
throw Error("Purchase not found, unable to abort with refund");
}
@@ -3595,7 +3440,7 @@ export class Wallet {
// From now on, we can't retry payment anymore,
// so mark this in the DB in case the /pay abort
// does not complete on the first try.
- await this.q().put(Stores.purchases, purchase);
+ await oneShotPut(this.db, Stores.purchases, purchase);
let resp;
@@ -3616,15 +3461,14 @@ export class Wallet {
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
await this.acceptRefundResponse(refundResponse);
- const markAbortDone = (p: PurchaseRecord) => {
+ await runWithWriteTransaction(this.db, [Stores.purchases], async (tx) => {
+ const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
+ if (!p) {
+ return;
+ }
p.abortDone = true;
- return p;
- };
- await this.q().mutate(
- Stores.purchases,
- purchase.contractTermsHash,
- markAbortDone,
- );
+ await tx.put(Stores.purchases, p);
+ });
}
/**
@@ -3684,7 +3528,7 @@ export class Wallet {
}
async getPurchaseDetails(hc: string): Promise<PurchaseDetails> {
- const purchase = await this.q().get(Stores.purchases, hc);
+ const purchase = await oneShotGet(this.db, Stores.purchases, hc);
if (!purchase) {
throw Error("unknown purchase");
}
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index e632cd38b..b227ca816 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -34,8 +34,7 @@ import {
CoinRecord,
DenominationRecord,
ExchangeRecord,
- ExchangeWireFeesRecord,
- TipRecord,
+ ExchangeWireInfo,
} from "./dbTypes";
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
@@ -98,7 +97,7 @@ export interface ReserveCreationInfo {
/**
* Wire fees from the exchange.
*/
- wireFees: ExchangeWireFeesRecord;
+ wireFees: ExchangeWireInfo;
/**
* Does the wallet know about an auditor for
@@ -475,7 +474,6 @@ export interface PreparePayResultError {
error: string;
}
-
export interface PreparePayResultPaid {
status: "paid";
contractTerms: ContractTerms;
@@ -517,18 +515,40 @@ export interface WalletDiagnostics {
}
export interface PendingWithdrawOperation {
- type: "withdraw"
+ type: "withdraw";
}
export interface PendingRefreshOperation {
- type: "refresh"
+ type: "refresh";
}
export interface PendingPayOperation {
- type: "pay"
+ type: "pay";
+}
+
+export interface OperationError {
+ type: string;
+ message: string;
+ details: any;
+}
+
+export interface PendingExchangeUpdateOperation {
+ type: "exchange-update";
+ stage: string;
+ exchangeBaseUrl: string;
+ lastError?: OperationError;
+}
+
+export interface PendingBugOperation {
+ type: "bug";
+ message: string;
+ details: any;
}
-export type PendingOperationInfo = PendingWithdrawOperation
+export type PendingOperationInfo =
+ | PendingWithdrawOperation
+ | PendingBugOperation
+ | PendingExchangeUpdateOperation;
export interface PendingOperationsResponse {
pendingOperations: PendingOperationInfo[];
@@ -541,4 +561,24 @@ export interface HistoryQuery {
* Level 1: All events.
*/
level: number;
-} \ No newline at end of file
+}
+
+export interface Timestamp {
+ /**
+ * Timestamp in milliseconds.
+ */
+ t_ms: number;
+}
+
+export interface Duration {
+ /**
+ * Duration in milliseconds.
+ */
+ d_ms: number;
+}
+
+export function getTimestampNow(): Timestamp {
+ return {
+ t_ms: new Date().getTime(),
+ };
+}
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 78a1a1fd0..3f6e5cc4a 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -73,14 +73,6 @@ export interface MessageMap {
request: { baseUrl: string };
response: dbTypes.ExchangeRecord;
};
- "currency-info": {
- request: { name: string };
- response: dbTypes.CurrencyRecord;
- };
- "hash-contract": {
- request: { contract: object };
- response: string;
- };
"reserve-creation-info": {
request: { baseUrl: string; amount: AmountJson };
response: walletTypes.ReserveCreationInfo;
@@ -145,14 +137,6 @@ export interface MessageMap {
request: {};
response: void;
};
- "log-and-display-error": {
- request: any;
- response: void;
- };
- "get-report": {
- request: { reportUid: string };
- response: void;
- };
"get-purchase-details": {
request: { contractTermsHash: string };
response: walletTypes.PurchaseDetails;
diff --git a/src/webex/pages/error.html b/src/webex/pages/error.html
deleted file mode 100644
index 51a8fd73a..000000000
--- a/src/webex/pages/error.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-
-<head>
- <meta charset="UTF-8">
- <title>Taler Wallet: Error Occured</title>
-
- <link rel="stylesheet" type="text/css" href="../style/wallet.css">
-
- <link rel="icon" href="/img/icon.png">
-
- <script src="/dist/page-common-bundle.js"></script>
- <script src="/dist/error-bundle.js"></script>
-
- <body>
- <div id="container"></div>
- </body>
-</html>
diff --git a/src/webex/pages/error.tsx b/src/webex/pages/error.tsx
deleted file mode 100644
index dee8ce44e..000000000
--- a/src/webex/pages/error.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- This file is part of TALER
- (C) 2015-2016 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/>
- */
-
-
-/**
- * Page shown to the user to confirm creation
- * of a reserve, usually requested by the bank.
- *
- * @author Florian Dold
- */
-
-
-import * as React from "react";
-import * as ReactDOM from "react-dom";
-import URI = require("urijs");
-
-import * as wxApi from "../wxApi";
-
-import { Collapsible } from "../renderHtml";
-
-interface ErrorProps {
- report: any;
-}
-
-class ErrorView extends React.Component<ErrorProps, { }> {
- render(): JSX.Element {
- const report = this.props.report;
- if (!report) {
- return (
- <div id="main">
- <h1>Error Report Not Found</h1>
- <p>This page is supposed to display an error reported by the GNU Taler wallet,
- but the corresponding error report can't be found.</p>
- <p>Maybe the error occured before the browser was restarted or the wallet was reloaded.</p>
- </div>
- );
- }
- try {
- switch (report.name) {
- case "pay-post-failed": {
- const summary = report.contractTerms.summary || report.contractTerms.order_id;
- return (
- <div id="main">
- <h1>Failed to send payment</h1>
- <p>
- Failed to send payment for <strong>{summary}</strong>{" "}
- to merchant <strong>{report.contractTerms.merchant.name}</strong>.
- </p>
- <p>
- You can <a href={report.contractTerms.fulfillment_url}>retry</a> the payment.{" "}
- If this problem persists, please contact the mechant with the error details below.
- </p>
- <Collapsible initiallyCollapsed={true} title="Error Details">
- <pre>
- {JSON.stringify(report, null, " ")}
- </pre>
- </Collapsible>
- </div>
- );
- }
- default:
- return (
- <div id="main">
- <h1>Unknown Error</h1>
- The GNU Taler wallet reported an unknown error. Here are the details:
- <pre>
- {JSON.stringify(report, null, " ")}
- </pre>
- </div>
- );
- }
- } catch (e) {
- return (
- <div id="main">
- <h1>Error</h1>
- The GNU Taler wallet reported an error. Here are the details:
- <pre>
- {JSON.stringify(report, null, " ")}
- </pre>
- A detailed error report could not be generated:
- <pre>
- {e.toString()}
- </pre>
- </div>
- );
- }
- }
-}
-
-async function main() {
- const url = new URI(document.location.href);
- const query: any = URI.parseQuery(url.query());
-
- const container = document.getElementById("container");
- if (!container) {
- console.error("fatal: can't mount component, countainer missing");
- return;
- }
-
- // report that we'll render, either looked up from the
- // logging module or synthesized here for fixed/fatal errors
- let report;
-
- const reportUid: string = query.reportUid;
- if (!reportUid) {
- report = {
- name: "missing-error",
- };
- } else {
- report = await wxApi.getReport(reportUid);
- }
-
- ReactDOM.render(<ErrorView report={report} />, container);
-}
-
-document.addEventListener("DOMContentLoaded", () => main());
diff --git a/src/webex/pages/logs.html b/src/webex/pages/logs.html
deleted file mode 100644
index 9545269e3..000000000
--- a/src/webex/pages/logs.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-
-<head>
- <meta charset="UTF-8">
- <title>Taler Wallet: Logs</title>
-
- <link rel="stylesheet" type="text/css" href="../style/wallet.css">
-
- <link rel="icon" href="/img/icon.png">
-
- <script src="/dist/page-common-bundle.js"></script>
- <script src="/dist/logs-bundle.js"></script>
-
- <style>
- .tree-item {
- margin: 2em;
- border-radius: 5px;
- border: 1px solid gray;
- padding: 1em;
- }
- </style>
-
- <body>
- <div id="container"></div>
- </body>
-</html>
diff --git a/src/webex/pages/logs.tsx b/src/webex/pages/logs.tsx
deleted file mode 100644
index c4fe670a2..000000000
--- a/src/webex/pages/logs.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 Inria
-
- 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/>
- */
-
-/**
- * Show wallet logs.
- *
- * @author Florian Dold
- */
-
-import {
- LogEntry,
- getLogs,
-} from "../../logging";
-
-import * as React from "react";
-import * as ReactDOM from "react-dom";
-
-interface LogViewProps {
- log: LogEntry;
-}
-
-class LogView extends React.Component<LogViewProps, {}> {
- render(): JSX.Element {
- const e = this.props.log;
- return (
- <div className="tree-item">
- <ul>
- <li>level: {e.level}</li>
- <li>msg: {e.msg}</li>
- <li>id: {e.id || "unknown"}</li>
- <li>file: {e.source || "(unknown)"}</li>
- <li>line: {e.line || "(unknown)"}</li>
- <li>col: {e.col || "(unknown)"}</li>
- {(e.detail ? <li> detail: <pre>{e.detail}</pre></li> : [])}
- </ul>
- </div>
- );
- }
-}
-
-interface LogsState {
- logs: LogEntry[]|undefined;
-}
-
-class Logs extends React.Component<{}, LogsState> {
- constructor(props: {}) {
- super({});
- this.update();
- this.state = {} as any;
- }
-
- async update() {
- const logs = await getLogs();
- this.setState({logs});
- }
-
- render(): JSX.Element {
- const logs = this.state.logs;
- if (!logs) {
- return <span>...</span>;
- }
- return (
- <div className="tree-item">
- Logs:
- {logs.map((e) => <LogView log={e} />)}
- </div>
- );
- }
-}
-
-document.addEventListener("DOMContentLoaded", () => {
- ReactDOM.render(<Logs />, document.getElementById("container")!);
-});
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index e2f821058..f2cccfba6 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -137,12 +137,12 @@ function AuditorDetailsView(props: {
</p>
);
}
- if (rci.exchangeInfo.auditors.length === 0) {
+ if ((rci.exchangeInfo.details?.auditors ?? []).length === 0) {
return <p>The exchange is not audited by any auditors.</p>;
}
return (
<div>
- {rci.exchangeInfo.auditors.map(a => (
+ {(rci.exchangeInfo.details?.auditors ?? []).map(a => (
<div>
<h3>Auditor {a.auditor_url}</h3>
<p>
@@ -231,7 +231,7 @@ function FeeDetailsView(props: {
<div>
<h3>Overview</h3>
<p>
- Public key: <ExpanderText text={rci.exchangeInfo.masterPublicKey} />
+ Public key: <ExpanderText text={rci.exchangeInfo.details?.masterPublicKey ?? "??"} />
</p>
<p>
{i18n.str`Withdrawal fees:`} {withdrawFee}
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 39c31ca51..a50672131 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -123,13 +123,6 @@ export function getCurrencies(): Promise<CurrencyRecord[]> {
}
-/**
- * Get information about a specific currency.
- */
-export function getCurrency(name: string): Promise<CurrencyRecord|null> {
- return callBackend("currency-info", {name});
-}
-
/**
* Get information about a specific exchange.
@@ -225,12 +218,6 @@ export function submitPay(contractTermsHash: string, sessionId: string | undefin
return callBackend("submit-pay", { contractTermsHash, sessionId });
}
-/**
- * Hash a contract. Throws if its not a valid contract.
- */
-export function hashContract(contract: object): Promise<string> {
- return callBackend("hash-contract", { contract });
-}
/**
* Mark a reserve as confirmed.
@@ -285,25 +272,6 @@ export function returnCoins(args: { amount: AmountJson, exchange: string, sender
/**
- * Record an error report and display it in a tabl.
- *
- * If sameTab is set, the error report will be opened in the current tab,
- * otherwise in a new tab.
- */
-export function logAndDisplayError(args: any): Promise<void> {
- return callBackend("log-and-display-error", args);
-}
-
-/**
- * Get an error report from the logging database for the
- * given report UID.
- */
-export function getReport(reportUid: string): Promise<any> {
- return callBackend("get-report", { reportUid });
-}
-
-
-/**
* Look up a purchase in the wallet database from
* the contract terms hash.
*/
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 16cd2a78c..f4decbc60 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -24,7 +24,6 @@
* Imports.
*/
import { BrowserHttpLib } from "../http";
-import * as logging from "../logging";
import { AmountJson } from "../amounts";
import {
ConfirmReserveRequest,
@@ -138,22 +137,6 @@ async function handleMessage(
}
return needsWallet().updateExchangeFromUrl(detail.baseUrl);
}
- case "currency-info": {
- if (!detail.name) {
- return Promise.resolve({ error: "name missing" });
- }
- return needsWallet().getCurrencyRecord(detail.name);
- }
- case "hash-contract": {
- if (!detail.contract) {
- return Promise.resolve({ error: "contract missing" });
- }
- return needsWallet()
- .hashContract(detail.contract)
- .then(hash => {
- return hash;
- });
- }
case "reserve-creation-info": {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({ error: "bad url" });
@@ -243,20 +226,6 @@ async function handleMessage(
};
return resp;
}
- case "log-and-display-error":
- logging.storeReport(detail).then(reportUid => {
- const url = chrome.extension.getURL(
- `/src/webex/pages/error.html?reportUid=${reportUid}`,
- );
- if (detail.sameTab && sender && sender.tab && sender.tab.id) {
- chrome.tabs.update(detail.tabId, { url });
- } else {
- chrome.tabs.create({ url });
- }
- });
- return;
- case "get-report":
- return logging.getReport(detail.reportUid);
case "get-purchase-details": {
const contractTermsHash = detail.contractTermsHash;
if (!contractTermsHash) {
@@ -574,17 +543,6 @@ export async function wxMain() {
chrome.runtime.reload();
});
- window.onerror = (m, source, lineno, colno, error) => {
- logging.record(
- "error",
- "".concat(m as any, error as any),
- undefined,
- source || "(unknown)",
- lineno || 0,
- colno || 0,
- );
- };
-
chrome.tabs.query({}, tabs => {
console.log("got tabs", tabs);
for (const tab of tabs) {
diff --git a/tsconfig.json b/tsconfig.json
index e190e14b7..25087b600 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -53,7 +53,6 @@
"src/index.ts",
"src/libtoolVersion-test.ts",
"src/libtoolVersion.ts",
- "src/logging.ts",
"src/promiseUtils.ts",
"src/query.ts",
"src/talerTypes.ts",
@@ -72,8 +71,6 @@
"src/webex/pages/add-auditor.tsx",
"src/webex/pages/auditors.tsx",
"src/webex/pages/benchmark.tsx",
- "src/webex/pages/error.tsx",
- "src/webex/pages/logs.tsx",
"src/webex/pages/pay.tsx",
"src/webex/pages/payback.tsx",
"src/webex/pages/popup.tsx",
diff --git a/webpack.config.js b/webpack.config.js
index 096455887..ab893eaca 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -80,8 +80,6 @@ module.exports = function (env) {
"pay": "./src/webex/pages/pay.tsx",
"withdraw": "./src/webex/pages/withdraw.tsx",
"welcome": "./src/webex/pages/welcome.tsx",
- "error": "./src/webex/pages/error.tsx",
- "logs": "./src/webex/pages/logs.tsx",
"payback": "./src/webex/pages/payback.tsx",
"popup": "./src/webex/pages/popup.tsx",
"reset-required": "./src/webex/pages/reset-required.tsx",
diff --git a/yarn.lock b/yarn.lock
index 2e7ec95c9..dea3f786f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3378,10 +3378,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-idb-bridge@^0.0.10:
- version "0.0.10"
- resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.10.tgz#69d59550dc722f6bf62cb98a4d4a2f2b9e66653c"
- integrity sha512-AE8YipDGjnS+APSlupcPNCNslCAErRwfUgJ8aNZigmVIr3xzI0YPGWr6NC6UI6ofpQ1DczUlaKqA86m1R/COGQ==
+idb-bridge@^0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.11.tgz#ba2fbd24b7e6f7f4de8333ed12b0912e64dda308"
+ integrity sha512-fLlHce/WwT6eD3sc54gsfvM5fZqrhAPwBNH4uU/y6D0C1+0higH7OgC5/wploMhkmNYkQID3BMNZvSUBr0leSQ==
ieee754@^1.1.4:
version "1.1.13"