From aaf7e1338d6cdb1b4e01ad318938b3eaea2f922b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 30 Nov 2019 00:36:20 +0100 Subject: wallet robustness WIP --- src/headless/bank.ts | 31 +++++++ src/headless/clk.ts | 15 +++- src/headless/helpers.ts | 18 +--- src/headless/merchant.ts | 64 ++++++++++++-- src/headless/taler-wallet-cli.ts | 183 +++++++++++++++++++++++++++++++-------- 5 files changed, 251 insertions(+), 60 deletions(-) (limited to 'src/headless') diff --git a/src/headless/bank.ts b/src/headless/bank.ts index f35021003..36f61a71a 100644 --- a/src/headless/bank.ts +++ b/src/headless/bank.ts @@ -45,6 +45,37 @@ function makeId(length: number): string { export class Bank { constructor(private bankBaseUrl: string) {} + async generateWithdrawUri(bankUser: BankUser, amount: string): Promise { + const body = { + amount, + }; + + const reqUrl = new URI("api/withdraw-headless-uri") + .absoluteTo(this.bankBaseUrl) + .href(); + + const resp = await Axios({ + method: "post", + url: reqUrl, + data: body, + responseType: "json", + headers: { + "X-Taler-Bank-Username": bankUser.username, + "X-Taler-Bank-Password": bankUser.password, + }, + }); + + if (resp.status != 200) { + throw Error("failed to create bank reserve"); + } + + const withdrawUri = resp.data["taler_withdraw_uri"]; + if (!withdrawUri) { + throw Error("Bank's response did not include withdraw URI"); + } + return withdrawUri; + } + async createReserve( bankUser: BankUser, amount: string, diff --git a/src/headless/clk.ts b/src/headless/clk.ts index 4a568dc18..828eb24c0 100644 --- a/src/headless/clk.ts +++ b/src/headless/clk.ts @@ -29,6 +29,7 @@ export let STRING: Converter = new Converter(); export interface OptionArgs { help?: string; default?: T; + onPresentHandler?: (v: T) => void; } export interface ArgumentArgs { @@ -269,9 +270,6 @@ export class CommandGroup { } printHelp(progName: string, parents: CommandGroup[]) { - const chain: CommandGroup[] = Array.prototype.concat(parents, [ - this, - ]); let usageSpec = ""; for (let p of parents) { usageSpec += (p.name ?? progName) + " "; @@ -352,6 +350,7 @@ export class CommandGroup { process.exit(-1); throw Error("not reached"); } + foundOptions[d.name] = true; myArgs[d.name] = true; } else { if (r.value === undefined) { @@ -380,6 +379,7 @@ export class CommandGroup { } if (opt.isFlag) { myArgs[opt.name] = true; + foundOptions[opt.name] = true; } else { if (si == optShort.length - 1) { if (i === unparsedArgs.length - 1) { @@ -449,6 +449,13 @@ export class CommandGroup { } } + for (let option of this.options) { + const ph = option.args.onPresentHandler; + if (ph && foundOptions[option.name]) { + ph(myArgs[option.name]); + } + } + if (parsedArgs[this.argKey].help) { this.printHelp(progname, parents); process.exit(-1); @@ -546,7 +553,7 @@ export class Program { name: N, flagspec: string[], args: OptionArgs = {}, - ): Program> { + ): Program> { this.mainCommand.flag(name, flagspec, args); return this as any; } diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts index a38ef1dbe..9faf24daf 100644 --- a/src/headless/helpers.ts +++ b/src/headless/helpers.ts @@ -34,35 +34,30 @@ import { Bank } from "./bank"; import fs = require("fs"); import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker"; +import { Logger } from "../logging"; + +const logger = new Logger("helpers.ts"); -const enableTracing = false; class ConsoleBadge implements Badge { startBusy(): void { - enableTracing && console.log("NOTIFICATION: busy"); } stopBusy(): void { - enableTracing && console.log("NOTIFICATION: busy end"); } showNotification(): void { - enableTracing && console.log("NOTIFICATION: show"); } clearNotification(): void { - enableTracing && console.log("NOTIFICATION: cleared"); } } export class NodeHttpLib implements HttpRequestLibrary { async get(url: string): Promise { - enableTracing && console.log("making GET request to", url); try { const resp = await Axios({ method: "get", url: url, responseType: "json", }); - enableTracing && console.log("got response", resp.data); - enableTracing && console.log("resp type", typeof resp.data); return { responseJson: resp.data, status: resp.status, @@ -76,7 +71,6 @@ export class NodeHttpLib implements HttpRequestLibrary { url: string, body: any, ): Promise { - enableTracing && console.log("making POST request to", url); try { const resp = await Axios({ method: "post", @@ -84,8 +78,6 @@ export class NodeHttpLib implements HttpRequestLibrary { responseType: "json", data: body, }); - enableTracing && console.log("got response", resp.data); - enableTracing && console.log("resp type", typeof resp.data); return { responseJson: resp.data, status: resp.status, @@ -149,7 +141,6 @@ export async function getDefaultNodeWallet( } myBackend.afterCommitCallback = async () => { - console.log("DATABASE COMMITTED"); // Allow caller to stop persisting the wallet. if (args.persistentStoragePath === undefined) { return; @@ -219,7 +210,7 @@ export async function withdrawTestBalance( const bankUser = await bank.registerRandomUser(); - console.log("bank user", bankUser); + logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`) const exchangePaytoUri = await myWallet.getExchangePaytoUri( exchangeBaseUrl, @@ -234,6 +225,5 @@ export async function withdrawTestBalance( ); await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub }); - await myWallet.runUntilReserveDepleted(reservePub); } diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts index 889eb2d6a..423e3d09e 100644 --- a/src/headless/merchant.ts +++ b/src/headless/merchant.ts @@ -19,9 +19,9 @@ * Used mostly for integration tests. */ - /** - * Imports. - */ +/** + * Imports. + */ import axios from "axios"; import { CheckPaymentResponse } from "../talerTypes"; import URI = require("urijs"); @@ -30,10 +30,60 @@ import URI = require("urijs"); * Connection to the *internal* merchant backend. */ export class MerchantBackendConnection { - constructor( - public merchantBaseUrl: string, - public apiKey: string, - ) {} + async refund( + orderId: string, + reason: string, + refundAmount: string, + ): Promise { + const reqUrl = new URI("refund").absoluteTo(this.merchantBaseUrl).href(); + const refundReq = { + order_id: orderId, + reason, + refund: refundAmount, + }; + const resp = await axios({ + method: "post", + url: reqUrl, + data: refundReq, + responseType: "json", + headers: { + Authorization: `ApiKey ${this.apiKey}`, + }, + }); + if (resp.status != 200) { + throw Error("failed to do refund"); + } + console.log("response", resp.data); + const refundUri = resp.data.taler_refund_uri; + if (!refundUri) { + throw Error("no refund URI in response"); + } + return refundUri; + } + + constructor(public merchantBaseUrl: string, public apiKey: string) {} + + async authorizeTip(amount: string, justification: string) { + const reqUrl = new URI("tip-authorize").absoluteTo(this.merchantBaseUrl).href(); + const tipReq = { + amount, + justification, + }; + const resp = await axios({ + method: "post", + url: reqUrl, + data: tipReq, + responseType: "json", + headers: { + Authorization: `ApiKey ${this.apiKey}`, + }, + }); + const tipUri = resp.data.taler_tip_uri; + if (!tipUri) { + throw Error("response does not contain tip URI"); + } + return tipUri; + } async createOrder( amount: string, diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 90c04dd97..cb2ff055c 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -26,11 +26,16 @@ import { BridgeIDBFactory, MemoryBackend } from "idb-bridge"; import { Logger } from "../logging"; import * as Amounts from "../amounts"; import { decodeCrock } from "../crypto/talerCrypto"; +import { Bank } from "./bank"; const logger = new Logger("taler-wallet-cli.ts"); const walletDbPath = 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, @@ -78,7 +83,7 @@ async function doPay( } if (pay) { - const payRes = await wallet.confirmPay(result.proposalId!, undefined); + const payRes = await wallet.confirmPay(result.proposalId, undefined); console.log("paid!"); } else { console.log("not paying"); @@ -93,6 +98,12 @@ function applyVerbose(verbose: boolean) { } } +function printVersion() { + 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.", @@ -101,6 +112,9 @@ const walletCli = clk help: "Inhibit running certain operations, useful for debugging and testing.", }) + .flag("version", ["-v", "--version"], { + onPresentHandler: printVersion, + }) .flag("verbose", ["-V", "--verbose"], { help: "Enable verbose output.", }); @@ -133,12 +147,21 @@ async function withWallet( } walletCli - .subcommand("", "balance", { help: "Show wallet balance." }) + .subcommand("balance", "balance", { help: "Show wallet balance." }) + .flag("json", ["--json"], { + help: "Show raw JSON.", + }) .action(async args => { - console.log("balance command called"); await withWallet(args, async wallet => { const balance = await wallet.getBalances(); - console.log(JSON.stringify(balance, undefined, 2)); + if (args.balance.json) { + console.log(JSON.stringify(balance, undefined, 2)); + } else { + const currencies = Object.keys(balance.byCurrency).sort(); + for (const c of currencies) { + console.log(Amounts.toString(balance.byCurrency[c].available)); + } + } }); }); @@ -205,15 +228,8 @@ walletCli process.exit(1); return; } - const { confirmTransferUrl } = await wallet.acceptWithdrawal( - uri, - selectedExchange, - ); - if (confirmTransferUrl) { - console.log("please confirm the transfer at", confirmTransferUrl); - } - } else { - console.error("unrecognized URI"); + const res = await wallet.acceptWithdrawal(uri, selectedExchange); + await wallet.processReserve(res.reservePub); } }); }); @@ -258,13 +274,39 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", { advancedCli .subcommand("decode", "decode", { - help: "Decode base32-crockford", + help: "Decode base32-crockford.", }) .action(args => { - const enc = fs.readFileSync(0, 'utf8'); - fs.writeFileSync(1, decodeCrock(enc.trim())) + const enc = fs.readFileSync(0, "utf8"); + fs.writeFileSync(1, decodeCrock(enc.trim())); }); +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.preparePay(args.payPrepare.url); + switch (res.status) { + case "error": + console.log("error:", res.error); + break; + case "insufficient-balance": + console.log("insufficient balance"); + break; + case "paid": + console.log("already paid"); + break; + case "payment-possible": + console.log("payment possible"); + break; + default: + assertUnreachable(res); + } + }); + }); advancedCli .subcommand("refresh", "force-refresh", { @@ -288,7 +330,9 @@ advancedCli console.log(`coin ${coin.coinPub}`); console.log(` status ${coin.status}`); console.log(` exchange ${coin.exchangeBaseUrl}`); - console.log(` remaining amount ${Amounts.toString(coin.currentAmount)}`); + console.log( + ` remaining amount ${Amounts.toString(coin.currentAmount)}`, + ); } }); }); @@ -324,12 +368,11 @@ testCli return; } console.log("taler pay URI:", talerPayUri); - await withWallet(args, async (wallet) => { + await withWallet(args, async wallet => { await doPay(wallet, talerPayUri, { alwaysYes: true }); }); }); - testCli .subcommand("integrationtestCmd", "integrationtest", { help: "Run integration test with bank, exchange and merchant.", @@ -377,7 +420,74 @@ testCli }); testCli - .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode") + .subcommand("genTipUri", "gen-tip-uri", { + help: "Generate a taler://tip URI.", + }) + .requiredOption("amount", ["-a", "--amount"], clk.STRING, { + default: "TESTKUDOS:10", + }) + .action(async args => { + const merchantBackend = new MerchantBackendConnection( + "https://backend.test.taler.net/", + "sandbox", + ); + const tipUri = await merchantBackend.authorizeTip("TESTKUDOS:10", "test"); + console.log(tipUri); + }); + +testCli + .subcommand("genRefundUri", "gen-refund-uri", { + help: "Generate a taler://refund URI.", + }) + .requiredOption("amount", ["-a", "--amount"], clk.STRING, { + default: "TESTKUDOS:5", + }) + .requiredOption("refundAmount", ["-r", "--refund"], clk.STRING, { + default: "TESTKUDOS:3", + }) + .requiredOption("summary", ["-s", "--summary"], clk.STRING, { + default: "Test Payment (for refund)", + }) + .action(async args => { + const cmdArgs = args.genRefundUri; + 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; + } + await withWallet(args, async wallet => { + await doPay(wallet, talerPayUri, { alwaysYes: true }); + }); + const refundUri = await merchantBackend.refund( + orderResp.orderId, + "test refund", + cmdArgs.refundAmount, + ); + console.log(refundUri); + }); + +testCli + .subcommand("genPayUri", "gen-pay-uri", { + help: "Generate a taler://pay URI.", + }) + .flag("qrcode", ["--qr"], { + help: "Show a QR code with the taler://pay URI", + }) + .flag("wait", ["--wait"], { + help: "Wait until payment has completed", + }) .requiredOption("amount", ["-a", "--amount"], clk.STRING, { default: "TESTKUDOS:1", }) @@ -385,8 +495,7 @@ testCli default: "Test Payment", }) .action(async args => { - const cmdArgs = args.testMerchantQrcodeCmd; - applyVerbose(args.wallet.verbose); + const cmdArgs = args.genPayUri; console.log("creating order"); const merchantBackend = new MerchantBackendConnection( "https://backend.test.taler.net/", @@ -399,7 +508,6 @@ testCli ); console.log("created new order with order ID", orderResp.orderId); const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId); - const qrcode = qrcodeGenerator(0, "M"); const talerPayUri = checkPayResp.taler_pay_uri; if (!talerPayUri) { console.error("fatal: no taler pay URI received from backend"); @@ -407,18 +515,23 @@ testCli return; } console.log("taler pay URI:", talerPayUri); - qrcode.addData(talerPayUri); - qrcode.make(); - console.log(qrcode.createASCII()); - console.log("waiting for payment ..."); - while (1) { - await asyncSleep(500); - const checkPayResp2 = await merchantBackend.checkPayment( - orderResp.orderId, - ); - if (checkPayResp2.paid) { - console.log("payment successfully received!"); - break; + if (cmdArgs.qrcode) { + const qrcode = qrcodeGenerator(0, "M"); + qrcode.addData(talerPayUri); + qrcode.make(); + console.log(qrcode.createASCII()); + } + if (cmdArgs.wait) { + console.log("waiting for payment ..."); + while (1) { + await asyncSleep(500); + const checkPayResp2 = await merchantBackend.checkPayment( + orderResp.orderId, + ); + if (checkPayResp2.paid) { + console.log("payment successfully received!"); + break; + } } } }); -- cgit v1.2.3