WIP: simplify DB queries and error handling

This commit is contained in:
Florian Dold 2019-11-20 19:48:43 +01:00
parent faedf69762
commit 553da64990
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
21 changed files with 1178 additions and 2487 deletions

View File

@ -64,7 +64,7 @@
"@types/urijs": "^1.19.3", "@types/urijs": "^1.19.3",
"axios": "^0.19.0", "axios": "^0.19.0",
"commander": "^3.0.1", "commander": "^3.0.1",
"idb-bridge": "^0.0.10", "idb-bridge": "^0.0.11",
"qrcode-generator": "^1.4.3", "qrcode-generator": "^1.4.3",
"source-map-support": "^0.5.12", "source-map-support": "^0.5.12",
"urijs": "^1.18.10" "urijs": "^1.18.10"

View File

@ -1,3 +1,4 @@
{ {
"editor.tabSize": 2 "editor.tabSize": 2,
"typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@ -36,6 +36,7 @@ import {
} from "./talerTypes"; } from "./talerTypes";
import { Index, Store } from "./query"; import { Index, Store } from "./query";
import { Timestamp, OperationError } from "./walletTypes";
/** /**
* Current database version, should be incremented * 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 { export interface ExchangeDetails {
/**
* Base url of the exchange.
*/
baseUrl: string;
/** /**
* Master public key of the exchange. * Master public key of the exchange.
*/ */
@ -331,22 +329,60 @@ export interface ExchangeRecord {
*/ */
currency: string; currency: string;
/**
* Timestamp for last update.
*/
lastUpdateTime: number;
/**
* 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.)
*/
lastUsedTime: number;
/** /**
* Last observed protocol version. * Last observed protocol version.
*/ */
protocolVersion?: string; protocolVersion: string;
/**
* Timestamp for last update.
*/
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 {
/**
* Base url of the exchange.
*/
baseUrl: string;
/**
* Details, once known.
*/
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;
} }
/** /**
@ -554,21 +590,6 @@ export class ProposalDownloadRecord {
static checked: (obj: any) => ProposalDownloadRecord; static checked: (obj: any) => 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. * Status of a tip we got from a merchant.
*/ */
@ -931,12 +952,6 @@ export namespace Stores {
constructor() { constructor() {
super("exchanges", { keyPath: "baseUrl" }); super("exchanges", { keyPath: "baseUrl" });
} }
pubKeyIndex = new Index<string, ExchangeRecord>(
this,
"pubKeyIndex",
"masterPublicKey",
);
} }
class CoinsStore extends Store<CoinRecord> { 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> { class ReservesStore extends Store<ReserveRecord> {
constructor() { constructor() {
super("reserves", { keyPath: "reserve_pub" }); super("reserves", { keyPath: "reserve_pub" });
@ -1094,7 +1103,6 @@ export namespace Stores {
export const config = new ConfigStore(); export const config = new ConfigStore();
export const currencies = new CurrenciesStore(); export const currencies = new CurrenciesStore();
export const denominations = new DenominationsStore(); export const denominations = new DenominationsStore();
export const exchangeWireFees = new ExchangeWireFeesStore();
export const exchanges = new ExchangeStore(); export const exchanges = new ExchangeStore();
export const precoins = new Store<PreCoinRecord>("precoins", { export const precoins = new Store<PreCoinRecord>("precoins", {
keyPath: "coinPub", keyPath: "coinPub",

View File

@ -20,7 +20,6 @@
import process = require("process"); import process = require("process");
import path = require("path"); import path = require("path");
import readline = require("readline"); import readline = require("readline");
import { symlinkSync } from "fs";
class Converter<T> {} class Converter<T> {}
@ -54,6 +53,7 @@ interface ArgumentDef {
name: string; name: string;
conv: Converter<any>; conv: Converter<any>;
args: ArgumentArgs<any>; args: ArgumentArgs<any>;
required: boolean;
} }
interface SubcommandDef { interface SubcommandDef {
@ -181,7 +181,7 @@ export class CommandGroup<GN extends keyof any, TG> {
return this as any; return this as any;
} }
argument<N extends keyof any, V>( requiredArgument<N extends keyof any, V>(
name: N, name: N,
conv: Converter<V>, conv: Converter<V>,
args: ArgumentArgs<V> = {}, args: ArgumentArgs<V> = {},
@ -190,6 +190,22 @@ export class CommandGroup<GN extends keyof any, TG> {
args: args, args: args,
conv: conv, conv: conv,
name: name as string, 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); this.arguments.push(argDef);
return this as any; return this as any;
@ -401,10 +417,25 @@ export class CommandGroup<GN extends keyof any, TG> {
process.exit(-1); process.exit(-1);
throw Error("not reached"); throw Error("not reached");
} }
myArgs[d.name] = unparsedArgs[i];
posArgIndex++; 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) { for (let option of this.options) {
if (option.isFlag == false && option.required == true) { if (option.isFlag == false && option.required == true) {
if (!foundOptions[option.name]) { if (!foundOptions[option.name]) {
@ -433,9 +464,7 @@ export class CommandGroup<GN extends keyof any, TG> {
unparsedArgs.slice(i + 1), unparsedArgs.slice(i + 1),
parsedArgs, parsedArgs,
); );
} } else if (this.myAction) {
if (this.myAction) {
this.myAction(parsedArgs); this.myAction(parsedArgs);
} else { } else {
this.printHelp(progname, parents); 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, name: N,
conv: Converter<V>, conv: Converter<V>,
args: ArgumentArgs<V> = {}, args: ArgumentArgs<V> = {},
): Program<N, T & SubRecord<PN, N, 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; 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>( export function program<PN extends keyof any>(
argKey: PN, argKey: PN,
args: ProgramArgs = {}, args: ProgramArgs = {},
@ -532,6 +578,8 @@ export function program<PN extends keyof any>(
return new Program(argKey as string, args); return new Program(argKey as string, args);
} }
export function prompt(question: string): Promise<string> { export function prompt(question: string): Promise<string> {
const stdinReadline = readline.createInterface({ const stdinReadline = readline.createInterface({
input: process.stdin, input: process.stdin,

View File

@ -97,6 +97,28 @@ const walletCli = clk
help: "Enable verbose output.", 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 walletCli
.subcommand("testPayCmd", "test-pay", { help: "create contract and pay" }) .subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
.requiredOption("amount", ["-a", "--amount"], clk.STRING) .requiredOption("amount", ["-a", "--amount"], clk.STRING)
@ -135,15 +157,11 @@ walletCli
walletCli walletCli
.subcommand("", "balance", { help: "Show wallet balance." }) .subcommand("", "balance", { help: "Show wallet balance." })
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose);
console.log("balance command called"); console.log("balance command called");
const wallet = await getDefaultNodeWallet({ withWallet(args, async (wallet) => {
persistentStoragePath: walletDbPath, 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 walletCli
@ -153,29 +171,19 @@ walletCli
.requiredOption("limit", ["--limit"], clk.STRING) .requiredOption("limit", ["--limit"], clk.STRING)
.requiredOption("contEvt", ["--continue-with"], clk.STRING) .requiredOption("contEvt", ["--continue-with"], clk.STRING)
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose); withWallet(args, async (wallet) => {
console.log("history command called"); const history = await wallet.getHistory();
const wallet = await getDefaultNodeWallet({ console.log(JSON.stringify(history, undefined, 2));
persistentStoragePath: walletDbPath,
}); });
console.log("got wallet");
const history = await wallet.getHistory();
console.log(JSON.stringify(history, undefined, 2));
process.exit(0);
}); });
walletCli walletCli
.subcommand("", "pending", { help: "Show pending operations." }) .subcommand("", "pending", { help: "Show pending operations." })
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose); withWallet(args, async (wallet) => {
console.log("history command called"); const pending = await wallet.getPendingOperations();
const wallet = await getDefaultNodeWallet({ console.log(JSON.stringify(pending, undefined, 2));
persistentStoragePath: walletDbPath,
}); });
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> { async function asyncSleep(milliSeconds: number): Promise<void> {
@ -184,6 +192,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 walletCli
.subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode") .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
.requiredOption("amount", ["-a", "--amount"], clk.STRING, { .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
@ -279,7 +297,7 @@ walletCli
walletCli walletCli
.subcommand("withdrawUriCmd", "withdraw-uri") .subcommand("withdrawUriCmd", "withdraw-uri")
.argument("withdrawUri", clk.STRING) .requiredArgument("withdrawUri", clk.STRING)
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose); applyVerbose(args.wallet.verbose);
const cmdArgs = args.withdrawUriCmd; const cmdArgs = args.withdrawUriCmd;
@ -318,7 +336,7 @@ walletCli
walletCli walletCli
.subcommand("tipUriCmd", "tip-uri") .subcommand("tipUriCmd", "tip-uri")
.argument("uri", clk.STRING) .requiredArgument("uri", clk.STRING)
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose); applyVerbose(args.wallet.verbose);
const tipUri = args.tipUriCmd.uri; const tipUri = args.tipUriCmd.uri;
@ -334,7 +352,7 @@ walletCli
walletCli walletCli
.subcommand("refundUriCmd", "refund-uri") .subcommand("refundUriCmd", "refund-uri")
.argument("uri", clk.STRING) .requiredArgument("uri", clk.STRING)
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose); applyVerbose(args.wallet.verbose);
const refundUri = args.refundUriCmd.uri; const refundUri = args.refundUriCmd.uri;
@ -346,20 +364,38 @@ walletCli
wallet.stop(); wallet.stop();
}); });
const exchangesCli = walletCli const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
.subcommand("exchangesCmd", "exchanges", { help: "Manage exchanges.",
help: "Manage exchanges."
});
exchangesCli.subcommand("exchangesListCmd", "list", {
help: "List known 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 walletCli
.subcommand("payUriCmd", "pay-uri") .subcommand("payUriCmd", "pay-uri")
.argument("url", clk.STRING) .requiredArgument("url", clk.STRING)
.flag("autoYes", ["-y", "--yes"]) .flag("autoYes", ["-y", "--yes"])
.action(async args => { .action(async args => {
applyVerbose(args.wallet.verbose); applyVerbose(args.wallet.verbose);
@ -374,7 +410,7 @@ walletCli
}); });
const testCli = walletCli.subcommand("testingArgs", "testing", { const testCli = walletCli.subcommand("testingArgs", "testing", {
help: "Subcommands for testing GNU Taler deployments." help: "Subcommands for testing GNU Taler deployments.",
}); });
testCli testCli

View File

@ -25,6 +25,7 @@ import { AmountJson } from "./amounts";
import * as Amounts from "./amounts"; import * as Amounts from "./amounts";
import URI = require("urijs"); import URI = require("urijs");
import { Timestamp } from "./walletTypes";
/** /**
* Show an amount in a form suitable for the user. * Show an amount in a form suitable for the user.
@ -125,6 +126,19 @@ export function getTalerStampSec(stamp: string): number | null {
return parseInt(m[1], 10); return parseInt(m[1], 10);
} }
/**
* 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. * Check if a timestamp is in the right format.
*/ */

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,7 @@ import {
CoinRecord, CoinRecord,
DenominationRecord, DenominationRecord,
ExchangeRecord, ExchangeRecord,
ExchangeWireFeesRecord, ExchangeWireInfo,
TipRecord,
} from "./dbTypes"; } from "./dbTypes";
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes"; import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
@ -98,7 +97,7 @@ export interface ReserveCreationInfo {
/** /**
* Wire fees from the exchange. * Wire fees from the exchange.
*/ */
wireFees: ExchangeWireFeesRecord; wireFees: ExchangeWireInfo;
/** /**
* Does the wallet know about an auditor for * Does the wallet know about an auditor for
@ -475,7 +474,6 @@ export interface PreparePayResultError {
error: string; error: string;
} }
export interface PreparePayResultPaid { export interface PreparePayResultPaid {
status: "paid"; status: "paid";
contractTerms: ContractTerms; contractTerms: ContractTerms;
@ -517,18 +515,40 @@ export interface WalletDiagnostics {
} }
export interface PendingWithdrawOperation { export interface PendingWithdrawOperation {
type: "withdraw" type: "withdraw";
} }
export interface PendingRefreshOperation { export interface PendingRefreshOperation {
type: "refresh" type: "refresh";
} }
export interface PendingPayOperation { export interface PendingPayOperation {
type: "pay" type: "pay";
} }
export type PendingOperationInfo = PendingWithdrawOperation 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
| PendingBugOperation
| PendingExchangeUpdateOperation;
export interface PendingOperationsResponse { export interface PendingOperationsResponse {
pendingOperations: PendingOperationInfo[]; pendingOperations: PendingOperationInfo[];
@ -542,3 +562,23 @@ export interface HistoryQuery {
*/ */
level: number; level: number;
} }
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(),
};
}

View File

@ -73,14 +73,6 @@ export interface MessageMap {
request: { baseUrl: string }; request: { baseUrl: string };
response: dbTypes.ExchangeRecord; response: dbTypes.ExchangeRecord;
}; };
"currency-info": {
request: { name: string };
response: dbTypes.CurrencyRecord;
};
"hash-contract": {
request: { contract: object };
response: string;
};
"reserve-creation-info": { "reserve-creation-info": {
request: { baseUrl: string; amount: AmountJson }; request: { baseUrl: string; amount: AmountJson };
response: walletTypes.ReserveCreationInfo; response: walletTypes.ReserveCreationInfo;
@ -145,14 +137,6 @@ export interface MessageMap {
request: {}; request: {};
response: void; response: void;
}; };
"log-and-display-error": {
request: any;
response: void;
};
"get-report": {
request: { reportUid: string };
response: void;
};
"get-purchase-details": { "get-purchase-details": {
request: { contractTermsHash: string }; request: { contractTermsHash: string };
response: walletTypes.PurchaseDetails; response: walletTypes.PurchaseDetails;

View File

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

View File

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

View File

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

View File

@ -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")!);
});

View File

@ -137,12 +137,12 @@ function AuditorDetailsView(props: {
</p> </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 <p>The exchange is not audited by any auditors.</p>;
} }
return ( return (
<div> <div>
{rci.exchangeInfo.auditors.map(a => ( {(rci.exchangeInfo.details?.auditors ?? []).map(a => (
<div> <div>
<h3>Auditor {a.auditor_url}</h3> <h3>Auditor {a.auditor_url}</h3>
<p> <p>
@ -231,7 +231,7 @@ function FeeDetailsView(props: {
<div> <div>
<h3>Overview</h3> <h3>Overview</h3>
<p> <p>
Public key: <ExpanderText text={rci.exchangeInfo.masterPublicKey} /> Public key: <ExpanderText text={rci.exchangeInfo.details?.masterPublicKey ?? "??"} />
</p> </p>
<p> <p>
{i18n.str`Withdrawal fees:`} {withdrawFee} {i18n.str`Withdrawal fees:`} {withdrawFee}

View File

@ -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. * Get information about a specific exchange.
@ -225,12 +218,6 @@ export function submitPay(contractTermsHash: string, sessionId: string | undefin
return callBackend("submit-pay", { contractTermsHash, sessionId }); 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. * Mark a reserve as confirmed.
@ -284,25 +271,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 * Look up a purchase in the wallet database from
* the contract terms hash. * the contract terms hash.

View File

@ -24,7 +24,6 @@
* Imports. * Imports.
*/ */
import { BrowserHttpLib } from "../http"; import { BrowserHttpLib } from "../http";
import * as logging from "../logging";
import { AmountJson } from "../amounts"; import { AmountJson } from "../amounts";
import { import {
ConfirmReserveRequest, ConfirmReserveRequest,
@ -138,22 +137,6 @@ async function handleMessage(
} }
return needsWallet().updateExchangeFromUrl(detail.baseUrl); 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": { case "reserve-creation-info": {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") { if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({ error: "bad url" }); return Promise.resolve({ error: "bad url" });
@ -243,20 +226,6 @@ async function handleMessage(
}; };
return resp; 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": { case "get-purchase-details": {
const contractTermsHash = detail.contractTermsHash; const contractTermsHash = detail.contractTermsHash;
if (!contractTermsHash) { if (!contractTermsHash) {
@ -574,17 +543,6 @@ export async function wxMain() {
chrome.runtime.reload(); 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 => { chrome.tabs.query({}, tabs => {
console.log("got tabs", tabs); console.log("got tabs", tabs);
for (const tab of tabs) { for (const tab of tabs) {

View File

@ -53,7 +53,6 @@
"src/index.ts", "src/index.ts",
"src/libtoolVersion-test.ts", "src/libtoolVersion-test.ts",
"src/libtoolVersion.ts", "src/libtoolVersion.ts",
"src/logging.ts",
"src/promiseUtils.ts", "src/promiseUtils.ts",
"src/query.ts", "src/query.ts",
"src/talerTypes.ts", "src/talerTypes.ts",
@ -72,8 +71,6 @@
"src/webex/pages/add-auditor.tsx", "src/webex/pages/add-auditor.tsx",
"src/webex/pages/auditors.tsx", "src/webex/pages/auditors.tsx",
"src/webex/pages/benchmark.tsx", "src/webex/pages/benchmark.tsx",
"src/webex/pages/error.tsx",
"src/webex/pages/logs.tsx",
"src/webex/pages/pay.tsx", "src/webex/pages/pay.tsx",
"src/webex/pages/payback.tsx", "src/webex/pages/payback.tsx",
"src/webex/pages/popup.tsx", "src/webex/pages/popup.tsx",

View File

@ -80,8 +80,6 @@ module.exports = function (env) {
"pay": "./src/webex/pages/pay.tsx", "pay": "./src/webex/pages/pay.tsx",
"withdraw": "./src/webex/pages/withdraw.tsx", "withdraw": "./src/webex/pages/withdraw.tsx",
"welcome": "./src/webex/pages/welcome.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", "payback": "./src/webex/pages/payback.tsx",
"popup": "./src/webex/pages/popup.tsx", "popup": "./src/webex/pages/popup.tsx",
"reset-required": "./src/webex/pages/reset-required.tsx", "reset-required": "./src/webex/pages/reset-required.tsx",

View File

@ -3378,10 +3378,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
idb-bridge@^0.0.10: idb-bridge@^0.0.11:
version "0.0.10" version "0.0.11"
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.10.tgz#69d59550dc722f6bf62cb98a4d4a2f2b9e66653c" resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.11.tgz#ba2fbd24b7e6f7f4de8333ed12b0912e64dda308"
integrity sha512-AE8YipDGjnS+APSlupcPNCNslCAErRwfUgJ8aNZigmVIr3xzI0YPGWr6NC6UI6ofpQ1DczUlaKqA86m1R/COGQ== integrity sha512-fLlHce/WwT6eD3sc54gsfvM5fZqrhAPwBNH4uU/y6D0C1+0higH7OgC5/wploMhkmNYkQID3BMNZvSUBr0leSQ==
ieee754@^1.1.4: ieee754@^1.1.4:
version "1.1.13" version "1.1.13"