diff options
Diffstat (limited to 'src/headless')
-rw-r--r-- | src/headless/clk.ts | 546 | ||||
-rw-r--r-- | src/headless/taler-wallet-cli.ts | 430 |
2 files changed, 790 insertions, 186 deletions
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(); |