work on CLI
This commit is contained in:
parent
87aa0f65c3
commit
d9297f3dfd
@ -50,7 +50,7 @@
|
||||
"through2": "3.0.1",
|
||||
"tslint": "^5.19.0",
|
||||
"typedoc": "^0.15.0",
|
||||
"typescript": "^3.6.2",
|
||||
"typescript": "^3.7.2",
|
||||
"uglify-js": "^3.0.27",
|
||||
"vinyl": "^2.2.0",
|
||||
"vinyl-fs": "^3.0.3",
|
||||
|
@ -896,6 +896,31 @@ export interface CoinsReturnRecord {
|
||||
wire: any;
|
||||
}
|
||||
|
||||
|
||||
export interface WithdrawalRecord {
|
||||
/**
|
||||
* Reserve that we're withdrawing from.
|
||||
*/
|
||||
reservePub: string;
|
||||
|
||||
/**
|
||||
* When was the withdrawal operation started started?
|
||||
* Timestamp in milliseconds.
|
||||
*/
|
||||
startTimestamp: number;
|
||||
|
||||
/**
|
||||
* When was the withdrawal operation completed?
|
||||
*/
|
||||
finishTimestamp?: number;
|
||||
|
||||
/**
|
||||
* Amount that is being withdrawn with this operation.
|
||||
* This does not include fees.
|
||||
*/
|
||||
withdrawalAmount: string;
|
||||
}
|
||||
|
||||
/* tslint:disable:completed-docs */
|
||||
|
||||
/**
|
||||
@ -1056,6 +1081,12 @@ export namespace Stores {
|
||||
}
|
||||
}
|
||||
|
||||
class WithdrawalsStore extends Store<WithdrawalRecord> {
|
||||
constructor() {
|
||||
super("withdrawals", { keyPath: "id", autoIncrement: true })
|
||||
}
|
||||
}
|
||||
|
||||
export const coins = new CoinsStore();
|
||||
export const coinsReturns = new Store<CoinsReturnRecord>("coinsReturns", {
|
||||
keyPath: "contractTermsHash",
|
||||
@ -1077,6 +1108,7 @@ export namespace Stores {
|
||||
export const purchases = new PurchasesStore();
|
||||
export const tips = new TipsStore();
|
||||
export const senderWires = new SenderWiresStore();
|
||||
export const withdrawals = new WithdrawalsStore();
|
||||
}
|
||||
|
||||
/* tslint:enable:completed-docs */
|
||||
|
546
src/headless/clk.ts
Normal file
546
src/headless/clk.ts
Normal file
@ -0,0 +1,546 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 GNUnet e.V.
|
||||
|
||||
GNU 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.
|
||||
|
||||
GNU 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
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import process = require("process");
|
||||
import path = require("path");
|
||||
import readline = require("readline");
|
||||
import { symlinkSync } from "fs";
|
||||
|
||||
class Converter<T> {}
|
||||
|
||||
export let INT = new Converter<number>();
|
||||
export let STRING: Converter<string> = new Converter<string>();
|
||||
|
||||
export interface OptionArgs<T> {
|
||||
help?: string;
|
||||
default?: T;
|
||||
}
|
||||
|
||||
export interface ArgumentArgs<T> {
|
||||
metavar?: string;
|
||||
help?: string;
|
||||
default?: T;
|
||||
}
|
||||
|
||||
export interface SubcommandArgs {
|
||||
help?: string;
|
||||
}
|
||||
|
||||
export interface FlagArgs {
|
||||
help?: string;
|
||||
}
|
||||
|
||||
export interface ProgramArgs {
|
||||
help?: string;
|
||||
}
|
||||
|
||||
interface ArgumentDef {
|
||||
name: string;
|
||||
conv: Converter<any>;
|
||||
args: ArgumentArgs<any>;
|
||||
}
|
||||
|
||||
interface SubcommandDef {
|
||||
commandGroup: CommandGroup<any, any>;
|
||||
name: string;
|
||||
args: SubcommandArgs;
|
||||
}
|
||||
|
||||
type ActionFn<TG> = (x: TG) => void;
|
||||
|
||||
type SubRecord<S extends keyof any, N extends keyof any, V> = {
|
||||
[Y in S]: { [X in N]: V };
|
||||
};
|
||||
|
||||
interface OptionDef {
|
||||
name: string;
|
||||
flagspec: string[];
|
||||
/**
|
||||
* Converter, only present for options, not for flags.
|
||||
*/
|
||||
conv?: Converter<any>;
|
||||
args: OptionArgs<any>;
|
||||
isFlag: boolean;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
function splitOpt(opt: string): { key: string; value?: string } {
|
||||
const idx = opt.indexOf("=");
|
||||
if (idx == -1) {
|
||||
return { key: opt };
|
||||
}
|
||||
return { key: opt.substring(0, idx), value: opt.substring(idx + 1) };
|
||||
}
|
||||
|
||||
function formatListing(key: string, value?: string): string {
|
||||
let res = " " + key;
|
||||
if (!value) {
|
||||
return res;
|
||||
}
|
||||
if (res.length >= 25) {
|
||||
return res + "\n" + " " + value;
|
||||
} else {
|
||||
return res.padEnd(24) + " " + value;
|
||||
}
|
||||
}
|
||||
|
||||
export class CommandGroup<GN extends keyof any, TG> {
|
||||
private shortOptions: { [name: string]: OptionDef } = {};
|
||||
private longOptions: { [name: string]: OptionDef } = {};
|
||||
private subcommandMap: { [name: string]: SubcommandDef } = {};
|
||||
private subcommands: SubcommandDef[] = [];
|
||||
private options: OptionDef[] = [];
|
||||
private arguments: ArgumentDef[] = [];
|
||||
|
||||
private myAction?: ActionFn<TG>;
|
||||
|
||||
constructor(
|
||||
private argKey: string,
|
||||
private name: string | null,
|
||||
private scArgs: SubcommandArgs,
|
||||
) {}
|
||||
|
||||
action(f: ActionFn<TG>) {
|
||||
if (this.myAction) {
|
||||
throw Error("only one action supported per command");
|
||||
}
|
||||
this.myAction = f;
|
||||
}
|
||||
|
||||
requiredOption<N extends keyof any, V>(
|
||||
name: N,
|
||||
flagspec: string[],
|
||||
conv: Converter<V>,
|
||||
args: OptionArgs<V> = {},
|
||||
): CommandGroup<GN, TG & SubRecord<GN, N, V>> {
|
||||
const def: OptionDef = {
|
||||
args: args,
|
||||
conv: conv,
|
||||
flagspec: flagspec,
|
||||
isFlag: false,
|
||||
required: true,
|
||||
name: name as string,
|
||||
};
|
||||
this.options.push(def);
|
||||
for (let flag of flagspec) {
|
||||
if (flag.startsWith("--")) {
|
||||
const flagname = flag.substring(2);
|
||||
this.longOptions[flagname] = def;
|
||||
} else if (flag.startsWith("-")) {
|
||||
const flagname = flag.substring(1);
|
||||
this.shortOptions[flagname] = def;
|
||||
} else {
|
||||
throw Error("option must start with '-' or '--'");
|
||||
}
|
||||
}
|
||||
return this as any;
|
||||
}
|
||||
|
||||
maybeOption<N extends keyof any, V>(
|
||||
name: N,
|
||||
flagspec: string[],
|
||||
conv: Converter<V>,
|
||||
args: OptionArgs<V> = {},
|
||||
): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> {
|
||||
const def: OptionDef = {
|
||||
args: args,
|
||||
conv: conv,
|
||||
flagspec: flagspec,
|
||||
isFlag: false,
|
||||
required: false,
|
||||
name: name as string,
|
||||
};
|
||||
this.options.push(def);
|
||||
for (let flag of flagspec) {
|
||||
if (flag.startsWith("--")) {
|
||||
const flagname = flag.substring(2);
|
||||
this.longOptions[flagname] = def;
|
||||
} else if (flag.startsWith("-")) {
|
||||
const flagname = flag.substring(1);
|
||||
this.shortOptions[flagname] = def;
|
||||
} else {
|
||||
throw Error("option must start with '-' or '--'");
|
||||
}
|
||||
}
|
||||
return this as any;
|
||||
}
|
||||
|
||||
argument<N extends keyof any, V>(
|
||||
name: N,
|
||||
conv: Converter<V>,
|
||||
args: ArgumentArgs<V> = {},
|
||||
): CommandGroup<GN, TG & SubRecord<GN, N, V>> {
|
||||
const argDef: ArgumentDef = {
|
||||
args: args,
|
||||
conv: conv,
|
||||
name: name as string,
|
||||
};
|
||||
this.arguments.push(argDef);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
flag<N extends string, V>(
|
||||
name: N,
|
||||
flagspec: string[],
|
||||
args: OptionArgs<V> = {},
|
||||
): CommandGroup<GN, TG & SubRecord<GN, N, boolean>> {
|
||||
const def: OptionDef = {
|
||||
args: args,
|
||||
flagspec: flagspec,
|
||||
isFlag: true,
|
||||
required: false,
|
||||
name: name as string,
|
||||
};
|
||||
this.options.push(def);
|
||||
for (let flag of flagspec) {
|
||||
if (flag.startsWith("--")) {
|
||||
const flagname = flag.substring(2);
|
||||
this.longOptions[flagname] = def;
|
||||
} else if (flag.startsWith("-")) {
|
||||
const flagname = flag.substring(1);
|
||||
this.shortOptions[flagname] = def;
|
||||
} else {
|
||||
throw Error("option must start with '-' or '--'");
|
||||
}
|
||||
}
|
||||
return this as any;
|
||||
}
|
||||
|
||||
subcommand<GN extends keyof any>(
|
||||
argKey: GN,
|
||||
name: string,
|
||||
args: SubcommandArgs = {},
|
||||
): CommandGroup<GN, TG> {
|
||||
const cg = new CommandGroup<GN, {}>(argKey as string, name, args);
|
||||
const def: SubcommandDef = {
|
||||
commandGroup: cg,
|
||||
name: name as string,
|
||||
args: args,
|
||||
};
|
||||
cg.flag("help", ["-h", "--help"], {
|
||||
help: "Show this message and exit.",
|
||||
});
|
||||
this.subcommandMap[name as string] = def;
|
||||
this.subcommands.push(def);
|
||||
this.subcommands = this.subcommands.sort((x1, x2) => {
|
||||
const a = x1.name;
|
||||
const b = x2.name;
|
||||
if (a === b) {
|
||||
return 0;
|
||||
} else if (a < b) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
return cg as any;
|
||||
}
|
||||
|
||||
printHelp(progName: string, parents: CommandGroup<any, any>[]) {
|
||||
const chain: CommandGroup<any, any>[] = Array.prototype.concat(parents, [
|
||||
this,
|
||||
]);
|
||||
let usageSpec = "";
|
||||
for (let p of parents) {
|
||||
usageSpec += (p.name ?? progName) + " ";
|
||||
if (p.arguments.length >= 1) {
|
||||
usageSpec += "<ARGS...> ";
|
||||
}
|
||||
}
|
||||
usageSpec += (this.name ?? progName) + " ";
|
||||
if (this.subcommands.length != 0) {
|
||||
usageSpec += "COMMAND ";
|
||||
}
|
||||
for (let a of this.arguments) {
|
||||
const argName = a.args.metavar ?? a.name;
|
||||
usageSpec += `<${argName}> `;
|
||||
}
|
||||
usageSpec = usageSpec.trimRight();
|
||||
console.log(`Usage: ${usageSpec}`);
|
||||
if (this.scArgs.help) {
|
||||
console.log();
|
||||
console.log(this.scArgs.help);
|
||||
}
|
||||
if (this.options.length != 0) {
|
||||
console.log();
|
||||
console.log("Options:");
|
||||
for (let opt of this.options) {
|
||||
let optSpec = opt.flagspec.join(", ");
|
||||
if (!opt.isFlag) {
|
||||
optSpec = optSpec + "=VALUE";
|
||||
}
|
||||
console.log(formatListing(optSpec, opt.args.help));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.subcommands.length != 0) {
|
||||
console.log();
|
||||
console.log("Commands:");
|
||||
for (let subcmd of this.subcommands) {
|
||||
console.log(formatListing(subcmd.name, subcmd.args.help));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the (sub-)command with the given command line parameters.
|
||||
*/
|
||||
run(
|
||||
progname: string,
|
||||
parents: CommandGroup<any, any>[],
|
||||
unparsedArgs: string[],
|
||||
parsedArgs: any,
|
||||
) {
|
||||
let posArgIndex = 0;
|
||||
let argsTerminated = false;
|
||||
let i;
|
||||
let foundSubcommand: CommandGroup<any, any> | undefined = undefined;
|
||||
const myArgs: any = (parsedArgs[this.argKey] = {});
|
||||
const foundOptions: { [name: string]: boolean } = {};
|
||||
for (i = 0; i < unparsedArgs.length; i++) {
|
||||
const argVal = unparsedArgs[i];
|
||||
if (argsTerminated == false) {
|
||||
if (argVal === "--") {
|
||||
argsTerminated = true;
|
||||
continue;
|
||||
}
|
||||
if (argVal.startsWith("--")) {
|
||||
const opt = argVal.substring(2);
|
||||
const r = splitOpt(opt);
|
||||
const d = this.longOptions[r.key];
|
||||
if (!d) {
|
||||
const n = this.name ?? progname;
|
||||
console.error(`error: unknown option '--${r.key}' for ${n}`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
if (d.isFlag) {
|
||||
if (r.value !== undefined) {
|
||||
console.error(`error: flag '--${r.key}' does not take a value`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
myArgs[d.name] = true;
|
||||
} else {
|
||||
if (r.value === undefined) {
|
||||
if (i === unparsedArgs.length - 1) {
|
||||
console.error(`error: option '--${r.key}' needs an argument`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
myArgs[d.name] = unparsedArgs[i+1];
|
||||
i++;
|
||||
} else {
|
||||
myArgs[d.name] = r.value;
|
||||
}
|
||||
foundOptions[d.name] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (argVal.startsWith("-") && argVal != "-") {
|
||||
const optShort = argVal.substring(1);
|
||||
for (let si = 0; si < optShort.length; si++) {
|
||||
const chr = optShort[si];
|
||||
const opt = this.shortOptions[chr];
|
||||
if (!opt) {
|
||||
console.error(`error: option '-${chr}' not known`);
|
||||
process.exit(-1);
|
||||
}
|
||||
if (opt.isFlag) {
|
||||
myArgs[opt.name] = true;
|
||||
} else {
|
||||
if (si == optShort.length - 1) {
|
||||
if (i === unparsedArgs.length - 1) {
|
||||
console.error(`error: option '-${chr}' needs an argument`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
} else {
|
||||
myArgs[opt.name] = unparsedArgs[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
myArgs[opt.name] = optShort.substring(si + 1);
|
||||
}
|
||||
foundOptions[opt.name] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (this.subcommands.length != 0) {
|
||||
const subcmd = this.subcommandMap[argVal];
|
||||
if (!subcmd) {
|
||||
console.error(`error: unknown command '${argVal}'`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
foundSubcommand = subcmd.commandGroup;
|
||||
break;
|
||||
} else {
|
||||
const d = this.arguments[posArgIndex];
|
||||
if (!d) {
|
||||
const n = this.name ?? progname;
|
||||
console.error(`error: too many arguments for ${n}`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
posArgIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let option of this.options) {
|
||||
if (option.isFlag == false && option.required == true) {
|
||||
if (!foundOptions[option.name]) {
|
||||
if (option.args.default !== undefined) {
|
||||
parsedArgs[this.argKey] = option.args.default;
|
||||
} else {
|
||||
const name = option.flagspec.join(",")
|
||||
console.error(`error: missing option '${name}'`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedArgs[this.argKey].help) {
|
||||
this.printHelp(progname, parents);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
|
||||
if (foundSubcommand) {
|
||||
foundSubcommand.run(
|
||||
progname,
|
||||
Array.prototype.concat(parents, [this]),
|
||||
unparsedArgs.slice(i + 1),
|
||||
parsedArgs,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.myAction) {
|
||||
this.myAction(parsedArgs);
|
||||
} else {
|
||||
this.printHelp(progname, parents);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Program<PN extends keyof any, T> {
|
||||
private mainCommand: CommandGroup<any, any>;
|
||||
|
||||
constructor(argKey: string, args: ProgramArgs = {}) {
|
||||
this.mainCommand = new CommandGroup<any, any>(argKey, null, {
|
||||
help: args.help,
|
||||
});
|
||||
this.mainCommand.flag("help", ["-h", "--help"], {
|
||||
help: "Show this message and exit.",
|
||||
});
|
||||
}
|
||||
|
||||
run() {
|
||||
const args = process.argv;
|
||||
if (args.length < 2) {
|
||||
console.error(
|
||||
"Error while parsing command line arguments: not enough arguments",
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
const progname = path.basename(args[1]);
|
||||
const rest = args.slice(2);
|
||||
|
||||
this.mainCommand.run(progname, [], rest, {});
|
||||
}
|
||||
|
||||
subcommand<GN extends keyof any>(
|
||||
argKey: GN,
|
||||
name: string,
|
||||
args: SubcommandArgs = {},
|
||||
): CommandGroup<GN, T> {
|
||||
const cmd = this.mainCommand.subcommand(argKey, name as string, args);
|
||||
return cmd as any;
|
||||
}
|
||||
|
||||
requiredOption<N extends keyof any, V>(
|
||||
name: N,
|
||||
flagspec: string[],
|
||||
conv: Converter<V>,
|
||||
args: OptionArgs<V> = {},
|
||||
): Program<PN, T & SubRecord<PN, N, V>> {
|
||||
this.mainCommand.requiredOption(name, flagspec, conv, args);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
maybeOption<N extends keyof any, V>(
|
||||
name: N,
|
||||
flagspec: string[],
|
||||
conv: Converter<V>,
|
||||
args: OptionArgs<V> = {},
|
||||
): Program<PN, T & SubRecord<PN, N, V | undefined>> {
|
||||
this.mainCommand.maybeOption(name, flagspec, conv, args);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a flag (option without value) to the program.
|
||||
*/
|
||||
flag<N extends string>(
|
||||
name: N,
|
||||
flagspec: string[],
|
||||
args: OptionArgs<boolean> = {},
|
||||
): Program<N, T & SubRecord<PN, N, boolean>> {
|
||||
this.mainCommand.flag(name, flagspec, args);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a positional argument to the program.
|
||||
*/
|
||||
argument<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);
|
||||
return this as any;
|
||||
}
|
||||
}
|
||||
|
||||
export function program<PN extends keyof any>(
|
||||
argKey: PN,
|
||||
args: ProgramArgs = {},
|
||||
): Program<PN, {}> {
|
||||
return new Program(argKey as string, args);
|
||||
}
|
||||
|
||||
export function prompt(question: string): Promise<string> {
|
||||
const stdinReadline = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
stdinReadline.question(question, res => {
|
||||
resolve(res);
|
||||
stdinReadline.close();
|
||||
});
|
||||
});
|
||||
}
|
@ -14,32 +14,68 @@
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import commander = require("commander");
|
||||
import os = require("os");
|
||||
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
|
||||
import { MerchantBackendConnection } from "./merchant";
|
||||
import { runIntegrationTest } from "./integrationtest";
|
||||
import { Wallet } from "../wallet";
|
||||
import querystring = require("querystring");
|
||||
import qrcodeGenerator = require("qrcode-generator");
|
||||
import readline = require("readline");
|
||||
|
||||
const program = new commander.Command();
|
||||
program.version("0.0.1").option("--verbose", "enable verbose output", false);
|
||||
import * as clk from "./clk";
|
||||
|
||||
const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
|
||||
|
||||
function prompt(question: string): Promise<string> {
|
||||
const stdinReadline = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
stdinReadline.question(question, res => {
|
||||
resolve(res);
|
||||
stdinReadline.close();
|
||||
});
|
||||
});
|
||||
async function doPay(
|
||||
wallet: Wallet,
|
||||
payUrl: string,
|
||||
options: { alwaysYes: boolean } = { alwaysYes: true },
|
||||
) {
|
||||
const result = await wallet.preparePay(payUrl);
|
||||
if (result.status === "error") {
|
||||
console.error("Could not pay:", result.error);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
if (result.status === "insufficient-balance") {
|
||||
console.log("contract", result.contractTerms!);
|
||||
console.error("insufficient balance");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
if (result.status === "paid") {
|
||||
console.log("already paid!");
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
if (result.status === "payment-possible") {
|
||||
console.log("paying ...");
|
||||
} else {
|
||||
throw Error("not reached");
|
||||
}
|
||||
console.log("contract", result.contractTerms!);
|
||||
let pay;
|
||||
if (options.alwaysYes) {
|
||||
pay = true;
|
||||
} else {
|
||||
while (true) {
|
||||
const yesNoResp = (await clk.prompt("Pay? [Y/n]")).toLowerCase();
|
||||
if (yesNoResp === "" || yesNoResp === "y" || yesNoResp === "yes") {
|
||||
pay = true;
|
||||
break;
|
||||
} else if (yesNoResp === "n" || yesNoResp === "no") {
|
||||
pay = false;
|
||||
break;
|
||||
} else {
|
||||
console.log("please answer y/n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pay) {
|
||||
const payRes = await wallet.confirmPay(result.proposalId!, undefined);
|
||||
console.log("paid!");
|
||||
} else {
|
||||
console.log("not paying");
|
||||
}
|
||||
}
|
||||
|
||||
function applyVerbose(verbose: boolean) {
|
||||
@ -49,31 +85,57 @@ function applyVerbose(verbose: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command("test-withdraw")
|
||||
.option(
|
||||
"-e, --exchange <exchange-url>",
|
||||
"exchange base URL",
|
||||
"https://exchange.test.taler.net/",
|
||||
)
|
||||
.option("-a, --amount <withdraw-amt>", "amount to withdraw", "TESTKUDOS:10")
|
||||
.option("-b, --bank <bank-url>", "bank base URL", "https://bank.test.taler.net/")
|
||||
.description("withdraw test currency from the test bank")
|
||||
.action(async cmdObj => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("test-withdraw command called");
|
||||
const walletCli = clk
|
||||
.program("wallet", {
|
||||
help: "Command line interface for the GNU Taler wallet.",
|
||||
})
|
||||
.maybeOption("inhibit", ["--inhibit"], clk.STRING, {
|
||||
help:
|
||||
"Inhibit running certain operations, useful for debugging and testing.",
|
||||
})
|
||||
.flag("verbose", ["-V", "--verbose"], {
|
||||
help: "Enable verbose output.",
|
||||
});
|
||||
|
||||
walletCli
|
||||
.subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
|
||||
.requiredOption("amount", ["-a", "--amount"], clk.STRING)
|
||||
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
|
||||
default: "Test Payment",
|
||||
})
|
||||
.action(async args => {
|
||||
const cmdArgs = args.testPayCmd;
|
||||
console.log("creating order");
|
||||
const merchantBackend = new MerchantBackendConnection(
|
||||
"https://backend.test.taler.net/",
|
||||
"sandbox",
|
||||
);
|
||||
const orderResp = await merchantBackend.createOrder(
|
||||
cmdArgs.amount,
|
||||
cmdArgs.summary,
|
||||
"",
|
||||
);
|
||||
console.log("created new order with order ID", orderResp.orderId);
|
||||
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
|
||||
const talerPayUri = checkPayResp.taler_pay_uri;
|
||||
if (!talerPayUri) {
|
||||
console.error("fatal: no taler pay URI received from backend");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
console.log("taler pay URI:", talerPayUri);
|
||||
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
await withdrawTestBalance(wallet, cmdObj.amount, cmdObj.bank, cmdObj.exchange);
|
||||
process.exit(0);
|
||||
|
||||
await doPay(wallet, talerPayUri, { alwaysYes: true });
|
||||
});
|
||||
|
||||
program
|
||||
.command("balance")
|
||||
.description("show wallet balance")
|
||||
.action(async () => {
|
||||
applyVerbose(program.verbose);
|
||||
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,
|
||||
@ -84,12 +146,14 @@ program
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
program
|
||||
.command("history")
|
||||
.description("show wallet history")
|
||||
.action(async () => {
|
||||
applyVerbose(program.verbose);
|
||||
walletCli
|
||||
.subcommand("", "history", { help: "Show wallet event history." })
|
||||
.requiredOption("from", ["--from"], clk.STRING)
|
||||
.requiredOption("to", ["--to"], clk.STRING)
|
||||
.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,
|
||||
@ -100,26 +164,45 @@ program
|
||||
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,
|
||||
});
|
||||
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> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
setTimeout(() => resolve(), milliSeconds);
|
||||
});
|
||||
}
|
||||
|
||||
program
|
||||
.command("test-merchant-qrcode")
|
||||
.option("-a, --amount <spend-amt>", "amount to spend", "TESTKUDOS:1")
|
||||
.option("-s, --summary <summary>", "contract summary", "Test Payment")
|
||||
.action(async cmdObj => {
|
||||
applyVerbose(program.verbose);
|
||||
walletCli
|
||||
.subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
|
||||
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
|
||||
default: "TESTKUDOS:1",
|
||||
})
|
||||
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
|
||||
default: "Test Payment",
|
||||
})
|
||||
.action(async args => {
|
||||
const cmdArgs = args.testMerchantQrcodeCmd;
|
||||
applyVerbose(args.wallet.verbose);
|
||||
console.log("creating order");
|
||||
const merchantBackend = new MerchantBackendConnection(
|
||||
"https://backend.test.taler.net/",
|
||||
"sandbox",
|
||||
);
|
||||
const orderResp = await merchantBackend.createOrder(
|
||||
cmdObj.amount,
|
||||
cmdObj.summary,
|
||||
cmdArgs.amount,
|
||||
cmdArgs.summary,
|
||||
"",
|
||||
);
|
||||
console.log("created new order with order ID", orderResp.orderId);
|
||||
@ -148,164 +231,31 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("withdraw-uri <withdraw-uri>")
|
||||
.action(async (withdrawUrl, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("withdrawing", withdrawUrl);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
|
||||
const withdrawInfo = await wallet.getWithdrawalInfo(withdrawUrl);
|
||||
|
||||
console.log("withdraw info", withdrawInfo);
|
||||
|
||||
const selectedExchange = withdrawInfo.suggestedExchange;
|
||||
if (!selectedExchange) {
|
||||
console.error("no suggested exchange!");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
reservePub,
|
||||
confirmTransferUrl,
|
||||
} = await wallet.acceptWithdrawal(
|
||||
withdrawUrl,
|
||||
selectedExchange,
|
||||
);
|
||||
|
||||
if (confirmTransferUrl) {
|
||||
console.log("please confirm the transfer at", confirmTransferUrl);
|
||||
}
|
||||
|
||||
await wallet.processReserve(reservePub);
|
||||
|
||||
console.log("finished withdrawing");
|
||||
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
program
|
||||
.command("tip-uri <tip-uri>")
|
||||
.action(async (tipUri, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("getting tip", tipUri);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
const res = await wallet.getTipStatus(tipUri);
|
||||
console.log("tip status", res);
|
||||
await wallet.acceptTip(tipUri);
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
|
||||
|
||||
program
|
||||
.command("refund-uri <refund-uri>")
|
||||
.action(async (refundUri, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("getting refund", refundUri);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
await wallet.applyRefund(refundUri);
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
program
|
||||
.command("pay-uri <pay-uri")
|
||||
.option("-y, --yes", "automatically answer yes to prompts")
|
||||
.action(async (payUrl, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("paying for", payUrl);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
const result = await wallet.preparePay(payUrl);
|
||||
if (result.status === "error") {
|
||||
console.error("Could not pay:", result.error);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
if (result.status === "insufficient-balance") {
|
||||
console.log("contract", result.contractTerms!);
|
||||
console.error("insufficient balance");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
if (result.status === "paid") {
|
||||
console.log("already paid!");
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
if (result.status === "payment-possible") {
|
||||
console.log("paying ...");
|
||||
} else {
|
||||
throw Error("not reached");
|
||||
}
|
||||
console.log("contract", result.contractTerms!);
|
||||
let pay;
|
||||
if (cmdObj.yes) {
|
||||
pay = true;
|
||||
} else {
|
||||
while (true) {
|
||||
const yesNoResp = (await prompt("Pay? [Y/n]")).toLowerCase();
|
||||
if (yesNoResp === "" || yesNoResp === "y" || yesNoResp === "yes") {
|
||||
pay = true;
|
||||
break;
|
||||
} else if (yesNoResp === "n" || yesNoResp === "no") {
|
||||
pay = false;
|
||||
break;
|
||||
} else {
|
||||
console.log("please answer y/n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pay) {
|
||||
const payRes = await wallet.confirmPay(result.proposalId!, undefined);
|
||||
console.log("paid!");
|
||||
} else {
|
||||
console.log("not paying");
|
||||
}
|
||||
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
program
|
||||
.command("integrationtest")
|
||||
.option(
|
||||
"-e, --exchange <exchange-url>",
|
||||
"exchange base URL",
|
||||
"https://exchange.test.taler.net/",
|
||||
)
|
||||
.option(
|
||||
"-m, --merchant <merchant-url>",
|
||||
"merchant base URL",
|
||||
"https://backend.test.taler.net/",
|
||||
)
|
||||
.option(
|
||||
"-k, --merchant-api-key <merchant-api-key>",
|
||||
"merchant API key",
|
||||
"sandbox",
|
||||
)
|
||||
.option(
|
||||
"-b, --bank <bank-url>",
|
||||
"bank base URL",
|
||||
"https://bank.test.taler.net/",
|
||||
)
|
||||
.option(
|
||||
"-w, --withdraw-amount <withdraw-amt>",
|
||||
"amount to withdraw",
|
||||
"TESTKUDOS:10",
|
||||
)
|
||||
.option("-s, --spend-amount <spend-amt>", "amount to spend", "TESTKUDOS:4")
|
||||
.description("Run integration test with bank, exchange and merchant.")
|
||||
.action(async cmdObj => {
|
||||
applyVerbose(program.verbose);
|
||||
walletCli
|
||||
.subcommand("integrationtestCmd", "integrationtest", {
|
||||
help: "Run integration test with bank, exchange and merchant.",
|
||||
})
|
||||
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
|
||||
default: "https://exchange.test.taler.net/",
|
||||
})
|
||||
.requiredOption("merchant", ["-m", "--merchant"], clk.STRING, {
|
||||
default: "https://backend.test.taler.net/",
|
||||
})
|
||||
.requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
|
||||
default: "sandbox",
|
||||
})
|
||||
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
|
||||
default: "https://bank.test.taler.net/",
|
||||
})
|
||||
.requiredOption("withdrawAmount", ["-b", "--bank"], clk.STRING, {
|
||||
default: "TESTKUDOS:10",
|
||||
})
|
||||
.requiredOption("spendAmount", ["-s", "--spend-amount"], clk.STRING, {
|
||||
default: "TESTKUDOS:4",
|
||||
})
|
||||
.action(async args => {
|
||||
applyVerbose(args.wallet.verbose);
|
||||
let cmdObj = args.integrationtestCmd;
|
||||
|
||||
try {
|
||||
await runIntegrationTest({
|
||||
@ -325,21 +275,129 @@ program
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// error on unknown commands
|
||||
program.on("command:*", function() {
|
||||
console.error(
|
||||
"Invalid command: %s\nSee --help for a list of available commands.",
|
||||
program.args.join(" "),
|
||||
);
|
||||
process.exit(1);
|
||||
walletCli
|
||||
.subcommand("withdrawUriCmd", "withdraw-uri")
|
||||
.argument("withdrawUri", clk.STRING)
|
||||
.action(async args => {
|
||||
applyVerbose(args.wallet.verbose);
|
||||
const cmdArgs = args.withdrawUriCmd;
|
||||
const withdrawUrl = cmdArgs.withdrawUri;
|
||||
console.log("withdrawing", withdrawUrl);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
|
||||
const withdrawInfo = await wallet.getWithdrawalInfo(withdrawUrl);
|
||||
|
||||
console.log("withdraw info", withdrawInfo);
|
||||
|
||||
const selectedExchange = withdrawInfo.suggestedExchange;
|
||||
if (!selectedExchange) {
|
||||
console.error("no suggested exchange!");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const { reservePub, confirmTransferUrl } = await wallet.acceptWithdrawal(
|
||||
withdrawUrl,
|
||||
selectedExchange,
|
||||
);
|
||||
|
||||
if (confirmTransferUrl) {
|
||||
console.log("please confirm the transfer at", confirmTransferUrl);
|
||||
}
|
||||
|
||||
await wallet.processReserve(reservePub);
|
||||
|
||||
console.log("finished withdrawing");
|
||||
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
walletCli
|
||||
.subcommand("tipUriCmd", "tip-uri")
|
||||
.argument("uri", clk.STRING)
|
||||
.action(async args => {
|
||||
applyVerbose(args.wallet.verbose);
|
||||
const tipUri = args.tipUriCmd.uri;
|
||||
console.log("getting tip", tipUri);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
const res = await wallet.getTipStatus(tipUri);
|
||||
console.log("tip status", res);
|
||||
await wallet.acceptTip(tipUri);
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
walletCli
|
||||
.subcommand("refundUriCmd", "refund-uri")
|
||||
.argument("uri", clk.STRING)
|
||||
.action(async args => {
|
||||
applyVerbose(args.wallet.verbose);
|
||||
const refundUri = args.refundUriCmd.uri;
|
||||
console.log("getting refund", refundUri);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
await wallet.applyRefund(refundUri);
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
const exchangesCli = walletCli
|
||||
.subcommand("exchangesCmd", "exchanges", {
|
||||
help: "Manage exchanges."
|
||||
});
|
||||
|
||||
exchangesCli.subcommand("exchangesListCmd", "list", {
|
||||
help: "List known exchanges."
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
exchangesCli.subcommand("exchangesListCmd", "update");
|
||||
|
||||
if (process.argv.length <= 2) {
|
||||
console.error("Error: No command given.");
|
||||
program.help();
|
||||
}
|
||||
walletCli
|
||||
.subcommand("payUriCmd", "pay-uri")
|
||||
.argument("url", clk.STRING)
|
||||
.flag("autoYes", ["-y", "--yes"])
|
||||
.action(async args => {
|
||||
applyVerbose(args.wallet.verbose);
|
||||
const payUrl = args.payUriCmd.url;
|
||||
console.log("paying for", payUrl);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
|
||||
await doPay(wallet, payUrl, { alwaysYes: args.payUriCmd.autoYes });
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
const testCli = walletCli.subcommand("testingArgs", "testing", {
|
||||
help: "Subcommands for testing GNU Taler deployments."
|
||||
});
|
||||
|
||||
testCli
|
||||
.subcommand("withdrawArgs", "withdraw", {
|
||||
help: "Withdraw from a test bank (must support test registrations).",
|
||||
})
|
||||
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
|
||||
default: "https://exchange.test.taler.net/",
|
||||
help: "Exchange base URL.",
|
||||
})
|
||||
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
|
||||
default: "https://bank.test.taler.net/",
|
||||
help: "Bank base URL",
|
||||
})
|
||||
.action(async args => {
|
||||
applyVerbose(args.wallet.verbose);
|
||||
console.log("balance command called");
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
console.log("got wallet");
|
||||
const balance = await wallet.getBalances();
|
||||
console.log(JSON.stringify(balance, undefined, 2));
|
||||
});
|
||||
|
||||
walletCli.run();
|
||||
|
118
src/wallet.ts
118
src/wallet.ts
@ -62,6 +62,7 @@ import {
|
||||
Stores,
|
||||
TipRecord,
|
||||
WireFee,
|
||||
WithdrawalRecord,
|
||||
} from "./dbTypes";
|
||||
import {
|
||||
Auditor,
|
||||
@ -106,6 +107,9 @@ import {
|
||||
WithdrawDetails,
|
||||
AcceptWithdrawalResponse,
|
||||
PurchaseDetails,
|
||||
PendingOperationInfo,
|
||||
PendingOperationsResponse,
|
||||
HistoryQuery,
|
||||
} from "./walletTypes";
|
||||
import { openPromise } from "./promiseUtils";
|
||||
import {
|
||||
@ -1159,6 +1163,9 @@ export class Wallet {
|
||||
return sp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reserve details
|
||||
*/
|
||||
private async sendReserveInfoToBank(reservePub: string) {
|
||||
const reserve = await this.q().get<ReserveRecord>(
|
||||
Stores.reserves,
|
||||
@ -1576,54 +1583,58 @@ export class Wallet {
|
||||
|
||||
console.log(`withdrawing ${denomsForWithdraw.length} coins`);
|
||||
|
||||
const ps = denomsForWithdraw.map(async denom => {
|
||||
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,
|
||||
denom.value,
|
||||
denom.feeWithdraw,
|
||||
).amount;
|
||||
const result = Amounts.sub(
|
||||
currentAmount,
|
||||
denom.value,
|
||||
denom.feeWithdraw,
|
||||
);
|
||||
if (result.saturated) {
|
||||
console.error("can't create precoin, saturated");
|
||||
throw AbortTransaction;
|
||||
}
|
||||
r.current_amount = result.amount;
|
||||
const stampMsNow = Math.floor(new Date().getTime());
|
||||
|
||||
// Reserve is depleted if the amount left is too small to withdraw
|
||||
if (Amounts.cmp(r.current_amount, smallestAmount) < 0) {
|
||||
r.timestamp_depleted = new Date().getTime();
|
||||
}
|
||||
const withdrawalRecord: WithdrawalRecord = {
|
||||
reservePub: reserve.reserve_pub,
|
||||
withdrawalAmount: Amounts.toString(withdrawAmount),
|
||||
startTimestamp: stampMsNow,
|
||||
}
|
||||
|
||||
return r;
|
||||
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
|
||||
|
||||
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;
|
||||
const result = Amounts.sub(currentAmount, totalWithdrawAmount);
|
||||
if (result.saturated) {
|
||||
console.error("can't create precoins, saturated");
|
||||
throw AbortTransaction;
|
||||
}
|
||||
r.current_amount = result.amount;
|
||||
|
||||
// Reserve is depleted if the amount left is too small to withdraw
|
||||
if (Amounts.cmp(r.current_amount, smallestAmount) < 0) {
|
||||
r.timestamp_depleted = new Date().getTime();
|
||||
}
|
||||
|
||||
const preCoin = await this.cryptoApi.createPreCoin(denom, reserve);
|
||||
return r;
|
||||
}
|
||||
|
||||
// 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()
|
||||
.put(Stores.precoins, preCoin)
|
||||
.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
|
||||
.finish();
|
||||
console.log("created precoin", preCoin.coinPub);
|
||||
} catch (e) {
|
||||
console.log("can't create pre-coin:", e.name, e.message);
|
||||
return;
|
||||
}
|
||||
await this.processPreCoin(preCoin.coinPub);
|
||||
});
|
||||
// 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();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(ps);
|
||||
for (let x of preCoinRecords) {
|
||||
await this.processPreCoin(x.coinPub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2701,7 +2712,7 @@ export class Wallet {
|
||||
/**
|
||||
* Retrive the full event history for this wallet.
|
||||
*/
|
||||
async getHistory(): Promise<{ history: HistoryRecord[] }> {
|
||||
async getHistory(historyQuery?: HistoryQuery): Promise<{ history: HistoryRecord[] }> {
|
||||
const history: HistoryRecord[] = [];
|
||||
|
||||
// FIXME: do pagination instead of generating the full history
|
||||
@ -2720,7 +2731,18 @@ export class Wallet {
|
||||
merchantName: p.contractTerms.merchant.name,
|
||||
},
|
||||
timestamp: p.timestamp,
|
||||
type: "offer-contract",
|
||||
type: "claim-order",
|
||||
});
|
||||
}
|
||||
|
||||
const withdrawals = await this.q().iter<WithdrawalRecord>(Stores.withdrawals).toArray()
|
||||
for (const w of withdrawals) {
|
||||
history.push({
|
||||
detail: {
|
||||
withdrawalAmount: w.withdrawalAmount,
|
||||
},
|
||||
timestamp: w.startTimestamp,
|
||||
type: "withdraw",
|
||||
});
|
||||
}
|
||||
|
||||
@ -2772,7 +2794,7 @@ export class Wallet {
|
||||
history.push({
|
||||
detail: {
|
||||
exchangeBaseUrl: r.exchange_base_url,
|
||||
requestedAmount: r.requested_amount,
|
||||
requestedAmount: Amounts.toString(r.requested_amount),
|
||||
reservePub: r.reserve_pub,
|
||||
},
|
||||
timestamp: r.created,
|
||||
@ -2812,6 +2834,12 @@ export class Wallet {
|
||||
return { history };
|
||||
}
|
||||
|
||||
async getPendingOperations(): Promise<PendingOperationsResponse> {
|
||||
return {
|
||||
pendingOperations: []
|
||||
};
|
||||
}
|
||||
|
||||
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
||||
const denoms = await this.q()
|
||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
|
||||
|
@ -515,3 +515,30 @@ export interface WalletDiagnostics {
|
||||
firefoxIdbProblem: boolean;
|
||||
dbOutdated: boolean;
|
||||
}
|
||||
|
||||
export interface PendingWithdrawOperation {
|
||||
type: "withdraw"
|
||||
}
|
||||
|
||||
export interface PendingRefreshOperation {
|
||||
type: "refresh"
|
||||
}
|
||||
|
||||
export interface PendingPayOperation {
|
||||
type: "pay"
|
||||
}
|
||||
|
||||
export type PendingOperationInfo = PendingWithdrawOperation
|
||||
|
||||
export interface PendingOperationsResponse {
|
||||
pendingOperations: PendingOperationInfo[];
|
||||
}
|
||||
|
||||
export interface HistoryQuery {
|
||||
/**
|
||||
* Verbosity of history events.
|
||||
* Level 0: Only withdraw, pay, tip and refund events.
|
||||
* Level 1: All events.
|
||||
*/
|
||||
level: number;
|
||||
}
|
@ -40,6 +40,7 @@
|
||||
"src/db.ts",
|
||||
"src/dbTypes.ts",
|
||||
"src/headless/bank.ts",
|
||||
"src/headless/clk.ts",
|
||||
"src/headless/helpers.ts",
|
||||
"src/headless/integrationtest.ts",
|
||||
"src/headless/merchant.ts",
|
||||
|
@ -6746,10 +6746,10 @@ typescript@3.5.x:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977"
|
||||
integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==
|
||||
|
||||
typescript@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.2.tgz#105b0f1934119dde543ac8eb71af3a91009efe54"
|
||||
integrity sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==
|
||||
typescript@^3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
|
||||
integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
|
||||
|
||||
uglify-js@^3.0.27, uglify-js@^3.1.4:
|
||||
version "3.6.0"
|
||||
|
Loading…
Reference in New Issue
Block a user