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 --- src/headless/NodeHttpLib.ts | 133 -------- src/headless/clk.ts | 614 ------------------------------------ src/headless/helpers.ts | 134 -------- src/headless/taler-wallet-cli.ts | 658 --------------------------------------- 4 files changed, 1539 deletions(-) delete mode 100644 src/headless/NodeHttpLib.ts delete mode 100644 src/headless/clk.ts delete mode 100644 src/headless/helpers.ts delete mode 100644 src/headless/taler-wallet-cli.ts (limited to 'src/headless') diff --git a/src/headless/NodeHttpLib.ts b/src/headless/NodeHttpLib.ts deleted file mode 100644 index d109c3b7c..000000000 --- a/src/headless/NodeHttpLib.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - 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 - - SPDX-License-Identifier: AGPL3.0-or-later -*/ - -/** - * Imports. - */ -import { - Headers, - HttpRequestLibrary, - HttpRequestOptions, - HttpResponse, -} from "../util/http"; -import { RequestThrottler } from "../util/RequestThrottler"; -import Axios from "axios"; -import { OperationFailedError, makeErrorDetails } from "../operations/errors"; -import { TalerErrorCode } from "../TalerErrorCode"; - -/** - * Implementation of the HTTP request library interface for node. - */ -export class NodeHttpLib implements HttpRequestLibrary { - private throttle = new RequestThrottler(); - private throttlingEnabled = true; - - /** - * Set whether requests should be throttled. - */ - setThrottling(enabled: boolean): void { - this.throttlingEnabled = enabled; - } - - private async req( - method: "post" | "get", - url: string, - body: any, - opt?: HttpRequestOptions, - ): Promise { - if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { - throw Error("request throttled"); - } - const resp = await Axios({ - method, - url: url, - responseType: "text", - headers: opt?.headers, - validateStatus: () => true, - transformResponse: (x) => x, - data: body, - }); - - const respText = resp.data; - if (typeof respText !== "string") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "unexpected response type", - { - httpStatusCode: resp.status, - requestUrl: url, - }, - ), - ); - } - const makeJson = async (): Promise => { - let responseJson; - try { - responseJson = JSON.parse(respText); - } catch (e) { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "invalid JSON", - { - httpStatusCode: resp.status, - requestUrl: url, - }, - ), - ); - } - if (responseJson === null || typeof responseJson !== "object") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "invalid JSON", - { - httpStatusCode: resp.status, - requestUrl: url, - }, - ), - ); - } - return responseJson; - }; - const headers = new Headers(); - for (const hn of Object.keys(resp.headers)) { - headers.set(hn, resp.headers[hn]); - } - return { - requestUrl: url, - headers, - status: resp.status, - text: async () => resp.data, - json: makeJson, - }; - } - - async get(url: string, opt?: HttpRequestOptions): Promise { - return this.req("get", url, undefined, opt); - } - - async postJson( - url: string, - body: any, - opt?: HttpRequestOptions, - ): Promise { - return this.req("post", url, body, opt); - } -} diff --git a/src/headless/clk.ts b/src/headless/clk.ts deleted file mode 100644 index a905464bd..000000000 --- a/src/headless/clk.ts +++ /dev/null @@ -1,614 +0,0 @@ -/* - 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/src/headless/helpers.ts b/src/headless/helpers.ts deleted file mode 100644 index 570ec9e69..000000000 --- a/src/headless/helpers.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - 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 - */ - -/** - * Helpers to create headless wallets. - * @author Florian Dold - */ - -/** - * Imports. - */ -import { Wallet } from "../wallet"; -import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge"; -import { openTalerDatabase } from "../db"; -import { HttpRequestLibrary } from "../util/http"; -import fs from "fs"; -import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker"; -import { WalletNotification } from "../types/notifications"; -import { Database } from "../util/query"; -import { NodeHttpLib } from "./NodeHttpLib"; -import { Logger } from "../util/logging"; -import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; - -const logger = new Logger("headless/helpers.ts"); - -export interface DefaultNodeWalletArgs { - /** - * Location of the wallet database. - * - * If not specified, the wallet starts out with an empty database and - * the wallet database is stored only in memory. - */ - persistentStoragePath?: string; - - /** - * Handler for asynchronous notifications from the wallet. - */ - notifyHandler?: (n: WalletNotification) => void; - - /** - * If specified, use this as HTTP request library instead - * of the default one. - */ - httpLib?: HttpRequestLibrary; -} - -/** - * Get a wallet instance with default settings for node. - */ -export async function getDefaultNodeWallet( - args: DefaultNodeWalletArgs = {}, -): Promise { - BridgeIDBFactory.enableTracing = false; - const myBackend = new MemoryBackend(); - myBackend.enableTracing = false; - - const storagePath = args.persistentStoragePath; - if (storagePath) { - try { - const dbContentStr: string = fs.readFileSync(storagePath, { - encoding: "utf-8", - }); - const dbContent = JSON.parse(dbContentStr); - myBackend.importDump(dbContent); - } catch (e) { - logger.warn("could not read wallet file"); - } - - myBackend.afterCommitCallback = async () => { - // Allow caller to stop persisting the wallet. - if (args.persistentStoragePath === undefined) { - return; - } - const dbContent = myBackend.exportDump(); - fs.writeFileSync(storagePath, JSON.stringify(dbContent, undefined, 2), { - encoding: "utf-8", - }); - }; - } - - BridgeIDBFactory.enableTracing = false; - - const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); - const myIdbFactory: IDBFactory = (myBridgeIdbFactory as any) as IDBFactory; - - let myHttpLib; - if (args.httpLib) { - myHttpLib = args.httpLib; - } else { - myHttpLib = new NodeHttpLib(); - } - - const myVersionChange = (): Promise => { - console.error("version change requested, should not happen"); - throw Error(); - }; - - shimIndexedDB(myBridgeIdbFactory); - - const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); - - let workerFactory; - try { - // Try if we have worker threads available, fails in older node versions. - require("worker_threads"); - workerFactory = new NodeThreadCryptoWorkerFactory(); - } catch (e) { - console.log( - "worker threads not available, falling back to synchronous workers", - ); - workerFactory = new SynchronousCryptoWorkerFactory(); - } - - const dbWrap = new Database(myDb); - - const w = new Wallet(dbWrap, myHttpLib, workerFactory); - if (args.notifyHandler) { - w.addNotificationListener(args.notifyHandler); - } - return w; -} diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts deleted file mode 100644 index a7f306ec3..000000000 --- a/src/headless/taler-wallet-cli.ts +++ /dev/null @@ -1,658 +0,0 @@ -/* - 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 } from "./helpers"; -import { Wallet } from "../wallet"; -import qrcodeGenerator from "qrcode-generator"; -import * as clk from "./clk"; -import { BridgeIDBFactory } from "idb-bridge"; -import { Logger } from "../util/logging"; -import { Amounts } from "../util/amounts"; -import { - decodeCrock, - setupRefreshPlanchet, - encodeCrock, -} from "../crypto/talerCrypto"; -import { - OperationFailedAndReportedError, - OperationFailedError, -} from "../operations/errors"; -import { classifyTalerUri, TalerUriType } from "../util/taleruri"; -import { Configuration } from "../util/talerconfig"; -import { setDangerousTimetravel } from "../util/time"; -import { makeCodecForList, codecForString } from "../util/codec"; -import { NodeHttpLib } from "./NodeHttpLib"; -import * as nacl from "../crypto/primitives/nacl-fast"; -import { addPaytoQueryParams } from "../util/payto"; -import { handleCoreApiRequest } from "../walletCoreApiHandler"; -import { PreparePayResultType } from "../types/walletTypes"; - -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 === PreparePayResultType.InsufficientBalance) { - console.log("contract", result.contractTerms); - console.error("insufficient balance"); - process.exit(1); - return; - } - if (result.status === 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 { - if (verbose) { - console.log("enabled verbose logging"); - BridgeIDBFactory.enableTracing = true; - } -} - -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`); - 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 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 = classifyTalerUri(uri); - switch (uriType) { - case TalerUriType.TalerPay: - await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes }); - break; - case TalerUriType.TalerTip: - { - const res = await wallet.getTipStatus(uri); - console.log("tip status", res); - await wallet.acceptTip(res.tipId); - } - break; - case TalerUriType.TalerRefund: - await wallet.applyRefund(uri); - break; - case 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, 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 = 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 PreparePayResultType.InsufficientBalance: - console.log("insufficient balance"); - break; - case PreparePayResultType.AlreadyConfirmed: - if (res.paid) { - console.log("already paid!"); - } else { - console.log("payment in progress"); - } - break; - case 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 = makeCodecForList(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) => { - const secretSeed = nacl.randomBytes(64); - const coinIndex = Math.ceil(Math.random() * 100); - const p = setupRefreshPlanchet(secretSeed, coinIndex); - console.log("setupRefreshPlanchet"); - console.log(` (in) secret seed: ${encodeCrock(secretSeed)}`); - console.log(` (in) coin index: ${coinIndex}`); - console.log(` (out) blinding secret: ${encodeCrock(p.bks)}`); - console.log(` (out) coin priv: ${encodeCrock(p.coinPriv)}`); - console.log(` (out) coin pub: ${encodeCrock(p.coinPub)}`); -}); - -walletCli.run(); -- cgit v1.2.3