From ffd2a62c3f7df94365980302fef3bc3376b48182 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 3 Aug 2020 13:00:48 +0530 Subject: modularize repo, use pnpm, improve typechecking --- packages/taler-wallet-cli/bin/taler-wallet-cli | 7 + packages/taler-wallet-cli/package.json | 48 ++ packages/taler-wallet-cli/rollup.config.js | 30 ++ packages/taler-wallet-cli/src/clk.ts | 614 ++++++++++++++++++++++++ packages/taler-wallet-cli/src/index.ts | 640 +++++++++++++++++++++++++ packages/taler-wallet-cli/tsconfig.json | 30 ++ 6 files changed, 1369 insertions(+) create mode 100755 packages/taler-wallet-cli/bin/taler-wallet-cli create mode 100644 packages/taler-wallet-cli/package.json create mode 100644 packages/taler-wallet-cli/rollup.config.js create mode 100644 packages/taler-wallet-cli/src/clk.ts create mode 100644 packages/taler-wallet-cli/src/index.ts create mode 100644 packages/taler-wallet-cli/tsconfig.json (limited to 'packages/taler-wallet-cli') diff --git a/packages/taler-wallet-cli/bin/taler-wallet-cli b/packages/taler-wallet-cli/bin/taler-wallet-cli new file mode 100755 index 000000000..871514024 --- /dev/null +++ b/packages/taler-wallet-cli/bin/taler-wallet-cli @@ -0,0 +1,7 @@ +#!/usr/bin/env node +try { + require('source-map-support').install(); +} catch (e) { + // Do nothing. +} +require('../dist/taler-wallet-cli.js') diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json new file mode 100644 index 000000000..1d4460021 --- /dev/null +++ b/packages/taler-wallet-cli/package.json @@ -0,0 +1,48 @@ +{ + "name": "taler-wallet-cli", + "version": "0.6.12", + "description": "", + "engines": { + "node": ">=0.12.0" + }, + "repository": { + "type": "git", + "url": "git://git.taler.net/wallet-core.git" + }, + "author": "Florian Dold", + "license": "GPL-3.0", + "main": "dist/taler-wallet-cli.js", + "bin": { + "taler-wallet-cli": "./bin/taler-wallet-cli" + }, + "scripts": { + "compile": "tsc && rollup -c", + "clean": "rimraf lib dist", + "pretty": "prettier --config ../../.prettierrc --write src" + }, + "files": [ + "AUTHORS", + "README", + "COPYING", + "bin/", + "dist/node", + "src/" + ], + "devDependencies": { + "@rollup/plugin-commonjs": "^14.0.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^8.4.0", + "@rollup/plugin-replace": "^2.3.3", + "rimraf": "^3.0.2", + "rollup": "^2.23.0", + "rollup-plugin-sourcemaps": "^0.6.2", + "rollup-plugin-terser": "^6.1.0", + "typedoc": "^0.17.8", + "typescript": "^3.9.7" + }, + "dependencies": { + "source-map-support": "^0.5.19", + "taler-wallet-core": "workspace:*", + "tslib": "^2.0.0" + } +} diff --git a/packages/taler-wallet-cli/rollup.config.js b/packages/taler-wallet-cli/rollup.config.js new file mode 100644 index 000000000..7cdca3b98 --- /dev/null +++ b/packages/taler-wallet-cli/rollup.config.js @@ -0,0 +1,30 @@ +// rollup.config.js +import commonjs from "@rollup/plugin-commonjs"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import json from "@rollup/plugin-json"; +import builtins from "builtin-modules"; +import pkg from "./package.json"; + +export default { + input: "lib/index.js", + output: { + file: pkg.main, + format: "cjs", + }, + external: builtins, + plugins: [ + nodeResolve({ + preferBuiltins: true, + }), + + commonjs({ + include: [/node_modules/, /dist/], + extensions: [".js", ".ts"], + ignoreGlobal: false, + sourceMap: false, + }), + + json(), + ], +} + diff --git a/packages/taler-wallet-cli/src/clk.ts b/packages/taler-wallet-cli/src/clk.ts new file mode 100644 index 000000000..a905464bd --- /dev/null +++ b/packages/taler-wallet-cli/src/clk.ts @@ -0,0 +1,614 @@ +/* + 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 + */ + +/** + * Imports. + */ +import process from "process"; +import path from "path"; +import readline from "readline"; + +class Converter {} + +export const INT = new Converter(); +export const STRING: Converter = new Converter(); + +export interface OptionArgs { + help?: string; + default?: T; + onPresentHandler?: (v: T) => void; +} + +export interface ArgumentArgs { + 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; + args: ArgumentArgs; + required: boolean; +} + +interface SubcommandDef { + commandGroup: CommandGroup; + name: string; + args: SubcommandArgs; +} + +type ActionFn = (x: TG) => void; + +type SubRecord = { + [Y in S]: { [X in N]: V }; +}; + +interface OptionDef { + name: string; + flagspec: string[]; + /** + * Converter, only present for options, not for flags. + */ + conv?: Converter; + args: OptionArgs; + 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 { + const res = " " + key; + if (!value) { + return res; + } + if (res.length >= 25) { + return res + "\n" + " " + value; + } else { + return res.padEnd(24) + " " + value; + } +} + +export class CommandGroup { + 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; + + constructor( + private argKey: string, + private name: string | null, + private scArgs: SubcommandArgs, + ) {} + + action(f: ActionFn): void { + if (this.myAction) { + throw Error("only one action supported per command"); + } + this.myAction = f; + } + + requiredOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): CommandGroup> { + const def: OptionDef = { + args: args, + conv: conv, + flagspec: flagspec, + isFlag: false, + required: true, + name: name as string, + }; + this.options.push(def); + for (const 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( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): CommandGroup> { + const def: OptionDef = { + args: args, + conv: conv, + flagspec: flagspec, + isFlag: false, + required: false, + name: name as string, + }; + this.options.push(def); + for (const 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; + } + + requiredArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): CommandGroup> { + const argDef: ArgumentDef = { + args: args, + conv: conv, + name: name as string, + required: true, + }; + this.arguments.push(argDef); + return this as any; + } + + maybeArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): CommandGroup> { + const argDef: ArgumentDef = { + args: args, + conv: conv, + name: name as string, + required: false, + }; + this.arguments.push(argDef); + return this as any; + } + + flag( + name: N, + flagspec: string[], + args: OptionArgs = {}, + ): CommandGroup> { + const def: OptionDef = { + args: args, + flagspec: flagspec, + isFlag: true, + required: false, + name: name as string, + }; + this.options.push(def); + for (const 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( + argKey: GN, + name: string, + args: SubcommandArgs = {}, + ): CommandGroup { + const cg = new CommandGroup(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[]): void { + let usageSpec = ""; + for (const p of parents) { + usageSpec += (p.name ?? progName) + " "; + if (p.arguments.length >= 1) { + usageSpec += " "; + } + } + usageSpec += (this.name ?? progName) + " "; + if (this.subcommands.length != 0) { + usageSpec += "COMMAND "; + } + for (const 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 (const 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 (const 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[], + unparsedArgs: string[], + parsedArgs: any, + ): void { + let posArgIndex = 0; + let argsTerminated = false; + let i; + let foundSubcommand: CommandGroup | undefined = undefined; + const myArgs: any = (parsedArgs[this.argKey] = {}); + const foundOptions: { [name: string]: boolean } = {}; + const currentName = this.name ?? progname; + 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) { + console.error( + `error: unknown option '--${r.key}' for ${currentName}`, + ); + 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"); + } + foundOptions[d.name] = true; + 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; + foundOptions[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) { + console.error(`error: too many arguments for ${currentName}`); + process.exit(-1); + throw Error("not reached"); + } + myArgs[d.name] = unparsedArgs[i]; + posArgIndex++; + } + } + + if (parsedArgs[this.argKey].help) { + this.printHelp(progname, parents); + process.exit(0); + throw Error("not reached"); + } + + for (let i = posArgIndex; i < this.arguments.length; i++) { + const d = this.arguments[i]; + if (d.required) { + if (d.args.default !== undefined) { + myArgs[d.name] = d.args.default; + } else { + console.error( + `error: missing positional argument '${d.name}' for ${currentName}`, + ); + process.exit(-1); + throw Error("not reached"); + } + } + } + + for (const option of this.options) { + if (option.isFlag == false && option.required == true) { + if (!foundOptions[option.name]) { + if (option.args.default !== undefined) { + myArgs[option.name] = option.args.default; + } else { + const name = option.flagspec.join(","); + console.error(`error: missing option '${name}'`); + process.exit(-1); + throw Error("not reached"); + } + } + } + } + + for (const option of this.options) { + const ph = option.args.onPresentHandler; + if (ph && foundOptions[option.name]) { + ph(myArgs[option.name]); + } + } + + if (foundSubcommand) { + foundSubcommand.run( + progname, + Array.prototype.concat(parents, [this]), + unparsedArgs.slice(i + 1), + parsedArgs, + ); + } else if (this.myAction) { + let r; + try { + r = this.myAction(parsedArgs); + } catch (e) { + console.error(`An error occured while running ${currentName}`); + console.error(e); + process.exit(1); + } + Promise.resolve(r).catch((e) => { + console.error(`An error occured while running ${currentName}`); + console.error(e); + process.exit(1); + }); + } else { + this.printHelp(progname, parents); + process.exit(-1); + throw Error("not reached"); + } + } +} + +export class Program { + private mainCommand: CommandGroup; + + constructor(argKey: string, args: ProgramArgs = {}) { + this.mainCommand = new CommandGroup(argKey, null, { + help: args.help, + }); + this.mainCommand.flag("help", ["-h", "--help"], { + help: "Show this message and exit.", + }); + } + + run(): void { + 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( + argKey: GN, + name: string, + args: SubcommandArgs = {}, + ): CommandGroup { + const cmd = this.mainCommand.subcommand(argKey, name as string, args); + return cmd as any; + } + + requiredOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): Program> { + this.mainCommand.requiredOption(name, flagspec, conv, args); + return this as any; + } + + maybeOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): Program> { + this.mainCommand.maybeOption(name, flagspec, conv, args); + return this as any; + } + + /** + * Add a flag (option without value) to the program. + */ + flag( + name: N, + flagspec: string[], + args: OptionArgs = {}, + ): Program> { + this.mainCommand.flag(name, flagspec, args); + return this as any; + } + + /** + * Add a required positional argument to the program. + */ + requiredArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): Program> { + this.mainCommand.requiredArgument(name, conv, args); + return this as any; + } + + /** + * Add an optional argument to the program. + */ + maybeArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): Program> { + this.mainCommand.maybeArgument(name, conv, args); + return this as any; + } +} + +export type GetArgType = T extends Program + ? AT + : T extends CommandGroup + ? AT + : any; + +export function program( + argKey: PN, + args: ProgramArgs = {}, +): Program { + return new Program(argKey as string, args); +} + +export function prompt(question: string): Promise { + const stdinReadline = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve, reject) => { + stdinReadline.question(question, (res) => { + resolve(res); + stdinReadline.close(); + }); + }); +} diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts new file mode 100644 index 000000000..c8e517e53 --- /dev/null +++ b/packages/taler-wallet-cli/src/index.ts @@ -0,0 +1,640 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. + + 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 + */ + +import os from "os"; +import fs from "fs"; +import { + getDefaultNodeWallet, + Logger, + Amounts, + Wallet, + OperationFailedAndReportedError, + OperationFailedError, + time, + taleruri, + walletTypes, + talerCrypto, + payto, + codec, + testvectors, + walletCoreApi, + NodeHttpLib, +} from "taler-wallet-core"; +import * as clk from "./clk"; + +const logger = new Logger("taler-wallet-cli.ts"); + +const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json"; + +function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} + +async function doPay( + wallet: Wallet, + payUrl: string, + options: { alwaysYes: boolean } = { alwaysYes: true }, +): Promise { + const result = await wallet.preparePayForUri(payUrl); + if (result.status === walletTypes.PreparePayResultType.InsufficientBalance) { + console.log("contract", result.contractTerms); + console.error("insufficient balance"); + process.exit(1); + return; + } + if (result.status === walletTypes.PreparePayResultType.AlreadyConfirmed) { + if (result.paid) { + console.log("already paid!"); + } else { + console.log("payment already in progress"); + } + + process.exit(0); + return; + } + if (result.status === "payment-possible") { + console.log("paying ..."); + } else { + throw Error("not reached"); + } + console.log("contract", result.contractTerms); + console.log("raw amount:", result.amountRaw); + console.log("effective amount:", result.amountEffective); + 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) { + await wallet.confirmPay(result.proposalId, undefined); + } else { + console.log("not paying"); + } +} + +function applyVerbose(verbose: boolean): void { + // TODO +} + +function printVersion(): void { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const info = require("../../../package.json"); + console.log(`${info.version}`); + process.exit(0); +} + +const walletCli = clk + .program("wallet", { + help: "Command line interface for the GNU Taler wallet.", + }) + .maybeOption("walletDbFile", ["--wallet-db"], clk.STRING, { + help: "location of the wallet database file", + }) + .maybeOption("timetravel", ["--timetravel"], clk.INT, { + help: "modify system time by given offset in microseconds", + onPresentHandler: (x) => { + // Convert microseconds to milliseconds and do timetravel + logger.info(`timetravelling ${x} microseconds`); + time.setDangerousTimetravel(x / 1000); + }, + }) + .maybeOption("inhibit", ["--inhibit"], clk.STRING, { + help: + "Inhibit running certain operations, useful for debugging and testing.", + }) + .flag("noThrottle", ["--no-throttle"], { + help: "Don't do any request throttling.", + }) + .flag("version", ["-v", "--version"], { + onPresentHandler: printVersion, + }) + .flag("verbose", ["-V", "--verbose"], { + help: "Enable verbose output.", + }); + +type WalletCliArgsType = clk.GetArgType; + +async function withWallet( + walletCliArgs: WalletCliArgsType, + f: (w: Wallet) => Promise, +): Promise { + const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; + const myHttpLib = new NodeHttpLib(); + if (walletCliArgs.wallet.noThrottle) { + myHttpLib.setThrottling(false); + } + const wallet = await getDefaultNodeWallet({ + persistentStoragePath: dbPath, + httpLib: myHttpLib, + }); + applyVerbose(walletCliArgs.wallet.verbose); + try { + await wallet.fillDefaults(); + const ret = await f(wallet); + return ret; + } catch (e) { + if ( + e instanceof OperationFailedAndReportedError || + e instanceof OperationFailedError + ) { + console.error("Operation failed: " + e.message); + console.error( + "Error details:", + JSON.stringify(e.operationError, undefined, 2), + ); + } else { + console.error("caught unhandled exception (bug?):", e); + } + process.exit(1); + } finally { + wallet.stop(); + } +} + +walletCli + .subcommand("balance", "balance", { help: "Show wallet balance." }) + .flag("json", ["--json"], { + help: "Show raw JSON.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const balance = await wallet.getBalances(); + console.log(JSON.stringify(balance, undefined, 2)); + }); + }); + +walletCli + .subcommand("api", "api", { help: "Call the wallet-core API directly." }) + .requiredArgument("operation", clk.STRING) + .requiredArgument("request", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + let requestJson; + try { + requestJson = JSON.parse(args.api.request); + } catch (e) { + console.error("Invalid JSON"); + process.exit(1); + } + const resp = await walletCoreApi.handleCoreApiRequest( + wallet, + args.api.operation, + "reqid-1", + requestJson, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +walletCli + .subcommand("", "pending", { help: "Show pending operations." }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const pending = await wallet.getPendingOperations(); + console.log(JSON.stringify(pending, undefined, 2)); + }); + }); + +walletCli + .subcommand("transactions", "transactions", { help: "Show transactions." }) + .maybeOption("currency", ["--currency"], clk.STRING) + .maybeOption("search", ["--search"], clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const pending = await wallet.getTransactions({ + currency: args.transactions.currency, + search: args.transactions.search, + }); + console.log(JSON.stringify(pending, undefined, 2)); + }); + }); + +async function asyncSleep(milliSeconds: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), milliSeconds); + }); +} + +walletCli + .subcommand("runPendingOpt", "run-pending", { + help: "Run pending operations.", + }) + .flag("forceNow", ["-f", "--force-now"]) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.runPending(args.runPendingOpt.forceNow); + }); + }); + +walletCli + .subcommand("finishPendingOpt", "run-until-done", { + help: "Run until no more work is left.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.runUntilDoneAndStop(); + }); + }); + +walletCli + .subcommand("handleUri", "handle-uri", { + help: "Handle a taler:// URI.", + }) + .requiredArgument("uri", clk.STRING) + .flag("autoYes", ["-y", "--yes"]) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const uri: string = args.handleUri.uri; + const uriType = taleruri.classifyTalerUri(uri); + switch (uriType) { + case taleruri.TalerUriType.TalerPay: + await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes }); + break; + case taleruri.TalerUriType.TalerTip: + { + const res = await wallet.getTipStatus(uri); + console.log("tip status", res); + await wallet.acceptTip(res.tipId); + } + break; + case taleruri.TalerUriType.TalerRefund: + await wallet.applyRefund(uri); + break; + case taleruri.TalerUriType.TalerWithdraw: + { + const withdrawInfo = await wallet.getWithdrawalDetailsForUri(uri); + const selectedExchange = withdrawInfo.defaultExchangeBaseUrl; + if (!selectedExchange) { + console.error("no suggested exchange!"); + process.exit(1); + return; + } + const res = await wallet.acceptWithdrawal(uri, selectedExchange); + await wallet.processReserve(res.reservePub); + } + break; + default: + console.log(`URI type (${uriType}) not handled`); + break; + } + return; + }); + }); + +const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", { + help: "Manage exchanges.", +}); + +exchangesCli + .subcommand("exchangesListCmd", "list", { + help: "List known exchanges.", + }) + .action(async (args) => { + console.log("Listing exchanges ..."); + await withWallet(args, async (wallet) => { + const exchanges = await wallet.getExchanges(); + console.log(JSON.stringify(exchanges, undefined, 2)); + }); + }); + +exchangesCli + .subcommand("exchangesUpdateCmd", "update", { + help: "Update or add an exchange by base URL.", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .flag("force", ["-f", "--force"]) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.updateExchangeFromUrl( + args.exchangesUpdateCmd.url, + args.exchangesUpdateCmd.force, + ); + }); + }); + +exchangesCli + .subcommand("exchangesAddCmd", "add", { + help: "Add an exchange by base URL.", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.updateExchangeFromUrl(args.exchangesAddCmd.url); + }); + }); + +exchangesCli + .subcommand("exchangesAcceptTosCmd", "accept-tos", { + help: "Accept terms of service.", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .requiredArgument("etag", clk.STRING, { + help: "ToS version tag to accept", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.acceptExchangeTermsOfService( + args.exchangesAcceptTosCmd.url, + args.exchangesAcceptTosCmd.etag, + ); + }); + }); + +exchangesCli + .subcommand("exchangesTosCmd", "tos", { + help: "Show terms of service.", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const tosResult = await wallet.getExchangeTos(args.exchangesTosCmd.url); + console.log(JSON.stringify(tosResult, undefined, 2)); + }); + }); + +const advancedCli = walletCli.subcommand("advancedArgs", "advanced", { + help: + "Subcommands for advanced operations (only use if you know what you're doing!).", +}); + +advancedCli + .subcommand("manualWithdrawalDetails", "manual-withdrawal-details", { + help: "Query withdrawal fees.", + }) + .requiredArgument("exchange", clk.STRING) + .requiredArgument("amount", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const details = await wallet.getWithdrawalDetailsForAmount( + args.manualWithdrawalDetails.exchange, + Amounts.parseOrThrow(args.manualWithdrawalDetails.amount), + ); + console.log(JSON.stringify(details, undefined, 2)); + }); + }); + +advancedCli + .subcommand("decode", "decode", { + help: "Decode base32-crockford.", + }) + .action((args) => { + const enc = fs.readFileSync(0, "utf8"); + fs.writeFileSync(1, talerCrypto.decodeCrock(enc.trim())); + }); + +advancedCli + .subcommand("withdrawManually", "withdraw-manually", { + help: "Withdraw manually from an exchange.", + }) + .requiredOption("exchange", ["--exchange"], clk.STRING, { + help: "Base URL of the exchange.", + }) + .requiredOption("amount", ["--amount"], clk.STRING, { + help: "Amount to withdraw", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const exchange = await wallet.updateExchangeFromUrl( + args.withdrawManually.exchange, + ); + const acct = exchange.wireInfo?.accounts[0]; + if (!acct) { + console.log("exchange has no accounts"); + return; + } + const reserve = await wallet.acceptManualWithdrawal( + exchange.baseUrl, + Amounts.parseOrThrow(args.withdrawManually.amount), + ); + const completePaytoUri = payto.addPaytoQueryParams(acct.payto_uri, { + amount: args.withdrawManually.amount, + message: `Taler top-up ${reserve.reservePub}`, + }); + console.log("Created reserve", reserve.reservePub); + console.log("Payto URI", completePaytoUri); + }); + }); + +const reservesCli = advancedCli.subcommand("reserves", "reserves", { + help: "Manage reserves.", +}); + +reservesCli + .subcommand("list", "list", { + help: "List reserves.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const reserves = await wallet.getReserves(); + console.log(JSON.stringify(reserves, undefined, 2)); + }); + }); + +reservesCli + .subcommand("update", "update", { + help: "Update reserve status via exchange.", + }) + .requiredArgument("reservePub", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.updateReserve(args.update.reservePub); + }); + }); + +advancedCli + .subcommand("payPrepare", "pay-prepare", { + help: "Claim an order but don't pay yet.", + }) + .requiredArgument("url", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const res = await wallet.preparePayForUri(args.payPrepare.url); + switch (res.status) { + case walletTypes.PreparePayResultType.InsufficientBalance: + console.log("insufficient balance"); + break; + case walletTypes.PreparePayResultType.AlreadyConfirmed: + if (res.paid) { + console.log("already paid!"); + } else { + console.log("payment in progress"); + } + break; + case walletTypes.PreparePayResultType.PaymentPossible: + console.log("payment possible"); + break; + default: + assertUnreachable(res); + } + }); + }); + +advancedCli + .subcommand("payConfirm", "pay-confirm", { + help: "Confirm payment proposed by a merchant.", + }) + .requiredArgument("proposalId", clk.STRING) + .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + wallet.confirmPay( + args.payConfirm.proposalId, + args.payConfirm.sessionIdOverride, + ); + }); + }); + +advancedCli + .subcommand("refresh", "force-refresh", { + help: "Force a refresh on a coin.", + }) + .requiredArgument("coinPub", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.refresh(args.refresh.coinPub); + }); + }); + +advancedCli + .subcommand("dumpCoins", "dump-coins", { + help: "Dump coins in an easy-to-process format.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const coinDump = await wallet.dumpCoins(); + console.log(JSON.stringify(coinDump, undefined, 2)); + }); + }); + +const coinPubListCodec = codec.makeCodecForList(codec.codecForString); + +advancedCli + .subcommand("suspendCoins", "suspend-coins", { + help: "Mark a coin as suspended, will not be used for payments.", + }) + .requiredArgument("coinPubSpec", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + let coinPubList: string[]; + try { + coinPubList = coinPubListCodec.decode( + JSON.parse(args.suspendCoins.coinPubSpec), + ); + } catch (e) { + console.log("could not parse coin list:", e.message); + process.exit(1); + } + for (const c of coinPubList) { + await wallet.setCoinSuspended(c, true); + } + }); + }); + +advancedCli + .subcommand("unsuspendCoins", "unsuspend-coins", { + help: "Mark a coin as suspended, will not be used for payments.", + }) + .requiredArgument("coinPubSpec", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + let coinPubList: string[]; + try { + coinPubList = coinPubListCodec.decode( + JSON.parse(args.unsuspendCoins.coinPubSpec), + ); + } catch (e) { + console.log("could not parse coin list:", e.message); + process.exit(1); + } + for (const c of coinPubList) { + await wallet.setCoinSuspended(c, false); + } + }); + }); + +advancedCli + .subcommand("coins", "list-coins", { + help: "List coins.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const coins = await wallet.getCoins(); + for (const coin of coins) { + console.log(`coin ${coin.coinPub}`); + console.log(` status ${coin.status}`); + console.log(` exchange ${coin.exchangeBaseUrl}`); + console.log(` denomPubHash ${coin.denomPubHash}`); + console.log( + ` remaining amount ${Amounts.stringify(coin.currentAmount)}`, + ); + } + }); + }); + +advancedCli + .subcommand("updateReserve", "update-reserve", { + help: "Update reserve status.", + }) + .requiredArgument("reservePub", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const r = await wallet.updateReserve(args.updateReserve.reservePub); + console.log("updated reserve:", JSON.stringify(r, undefined, 2)); + }); + }); + +advancedCli + .subcommand("updateReserve", "show-reserve", { + help: "Show the current reserve status.", + }) + .requiredArgument("reservePub", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const r = await wallet.getReserve(args.updateReserve.reservePub); + console.log("updated reserve:", JSON.stringify(r, undefined, 2)); + }); + }); + +const testCli = walletCli.subcommand("testingArgs", "testing", { + help: "Subcommands for testing GNU Taler deployments.", +}); + +testCli.subcommand("vectors", "vectors").action(async (args) => { + testvectors.printTestVectors(); +}); + +walletCli.run(); diff --git a/packages/taler-wallet-cli/tsconfig.json b/packages/taler-wallet-cli/tsconfig.json new file mode 100644 index 000000000..34767d1e0 --- /dev/null +++ b/packages/taler-wallet-cli/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "composite": true, + "target": "ES6", + "module": "ESNext", + "moduleResolution": "node", + "sourceMap": true, + "lib": ["es6"], + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictPropertyInitialization": false, + "outDir": "lib", + "noImplicitAny": true, + "noImplicitThis": true, + "incremental": true, + "esModuleInterop": true, + "importHelpers": true, + "rootDir": "src", + "baseUrl": "./src", + "types": ["node"] + }, + "include": ["src/**/*"], + "references": [ + { + "path": "../taler-wallet-core/" + } + ] +} -- cgit v1.2.3