gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: work on CLI


From: gnunet
Subject: [taler-wallet-core] branch master updated: work on CLI
Date: Tue, 19 Nov 2019 16:16:16 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new d9297f3d work on CLI
d9297f3d is described below

commit d9297f3dfddd5c7b072b46dee984251e3202ad75
Author: Florian Dold <address@hidden>
AuthorDate: Tue Nov 19 16:16:12 2019 +0100

    work on CLI
---
 package.json                     |   2 +-
 src/dbTypes.ts                   |  32 +++
 src/headless/clk.ts              | 546 +++++++++++++++++++++++++++++++++++++++
 src/headless/taler-wallet-cli.ts | 430 +++++++++++++++++-------------
 src/wallet.ts                    | 118 +++++----
 src/walletTypes.ts               |  27 ++
 tsconfig.json                    |   1 +
 yarn.lock                        |   8 +-
 8 files changed, 928 insertions(+), 236 deletions(-)

diff --git a/package.json b/package.json
index 94a70a02..8e7a5b35 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 ef79ae19..28893b8e 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 00000000..642a1bef
--- /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 8c31e67d..41f68319 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 bbeaca60..f5219c45 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 fddf0568..e632cd38 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 820dd560..e190e14b 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 31f7d3ef..2e7ec95c 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"

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]