summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json2
-rw-r--r--src/dbTypes.ts32
-rw-r--r--src/headless/clk.ts546
-rw-r--r--src/headless/taler-wallet-cli.ts430
-rw-r--r--src/wallet.ts118
-rw-r--r--src/walletTypes.ts27
-rw-r--r--tsconfig.json1
-rw-r--r--yarn.lock8
8 files changed, 928 insertions, 236 deletions
diff --git a/package.json b/package.json
index 94a70a020..8e7a5b355 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index ef79ae193..28893b8eb 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -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 */
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
new file mode 100644
index 000000000..642a1bef3
--- /dev/null
+++ b/src/headless/clk.ts
@@ -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();
+ });
+ });
+}
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 8c31e67d8..41f68319a 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -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,10 +231,59 @@ program
}
});
-program
- .command("withdraw-uri <withdraw-uri>")
- .action(async (withdrawUrl, 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({
+ amountToSpend: cmdObj.spendAmount,
+ amountToWithdraw: cmdObj.withdrawAmount,
+ bankBaseUrl: cmdObj.bank,
+ exchangeBaseUrl: cmdObj.exchange,
+ merchantApiKey: cmdObj.merchantApiKey,
+ merchantBaseUrl: cmdObj.merchant,
+ }).catch(err => {
+ console.error("Failed with exception:");
+ console.error(err);
+ });
+
+ process.exit(0);
+ } catch (e) {
+ console.error(e);
+ 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,
@@ -168,10 +300,7 @@ program
return;
}
- const {
- reservePub,
- confirmTransferUrl,
- } = await wallet.acceptWithdrawal(
+ const { reservePub, confirmTransferUrl } = await wallet.acceptWithdrawal(
withdrawUrl,
selectedExchange,
);
@@ -187,10 +316,12 @@ program
wallet.stop();
});
-program
- .command("tip-uri <tip-uri>")
- .action(async (tipUri, cmdObj) => {
- applyVerbose(program.verbose);
+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,
@@ -201,12 +332,12 @@ program
wallet.stop();
});
-
-
- program
- .command("refund-uri <refund-uri>")
- .action(async (refundUri, cmdObj) => {
- applyVerbose(program.verbose);
+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,
@@ -215,131 +346,58 @@ program
wallet.stop();
});
-program
- .command("pay-uri <pay-uri")
- .option("-y, --yes", "automatically answer yes to prompts")
- .action(async (payUrl, cmdObj) => {
- applyVerbose(program.verbose);
+const exchangesCli = walletCli
+ .subcommand("exchangesCmd", "exchanges", {
+ help: "Manage exchanges."
+ });
+
+exchangesCli.subcommand("exchangesListCmd", "list", {
+ help: "List known exchanges."
+});
+
+exchangesCli.subcommand("exchangesListCmd", "update");
+
+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,
});
- 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");
- }
+ await doPay(wallet, payUrl, { alwaysYes: args.payUriCmd.autoYes });
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);
-
- try {
- await runIntegrationTest({
- amountToSpend: cmdObj.spendAmount,
- amountToWithdraw: cmdObj.withdrawAmount,
- bankBaseUrl: cmdObj.bank,
- exchangeBaseUrl: cmdObj.exchange,
- merchantApiKey: cmdObj.merchantApiKey,
- merchantBaseUrl: cmdObj.merchant,
- }).catch(err => {
- console.error("Failed with exception:");
- console.error(err);
- });
-
- process.exit(0);
- } catch (e) {
- 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);
+const testCli = walletCli.subcommand("testingArgs", "testing", {
+ help: "Subcommands for testing GNU Taler deployments."
});
-program.parse(process.argv);
+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));
+ });
-if (process.argv.length <= 2) {
- console.error("Error: No command given.");
- program.help();
-}
+walletCli.run();
diff --git a/src/wallet.ts b/src/wallet.ts
index bbeaca601..f5219c459 100644
--- a/src/wallet.ts
+++ b/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 preCoin = 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
- // 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;
+ 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();
}
- await this.processPreCoin(preCoin.coinPub);
- });
- await Promise.all(ps);
+ 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()
+ .putAll(Stores.precoins, preCoinRecords)
+ .put(Stores.withdrawals, withdrawalRecord)
+ .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
+ .finish();
+ } catch (e) {
+ return;
+ }
+
+ 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)
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index fddf05680..e632cd38b 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -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;
+} \ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 820dd560a..e190e14b7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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",
diff --git a/yarn.lock b/yarn.lock
index 31f7d3ef0..2e7ec95c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"