aboutsummaryrefslogtreecommitdiff
path: root/src/headless
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-11-30 00:36:20 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-11-30 00:36:20 +0100
commitaaf7e1338d6cdb1b4e01ad318938b3eaea2f922b (patch)
tree594129ccdf20757aeb86d434dd62c0c1e8259ed5 /src/headless
parent809fa186448dbd924f258f89920b9336f1979bb0 (diff)
downloadwallet-core-aaf7e1338d6cdb1b4e01ad318938b3eaea2f922b.tar.xz
wallet robustness WIP
Diffstat (limited to 'src/headless')
-rw-r--r--src/headless/bank.ts31
-rw-r--r--src/headless/clk.ts15
-rw-r--r--src/headless/helpers.ts18
-rw-r--r--src/headless/merchant.ts64
-rw-r--r--src/headless/taler-wallet-cli.ts183
5 files changed, 251 insertions, 60 deletions
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<string> {
+ 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<string> = new Converter<string>();
export interface OptionArgs<T> {
help?: string;
default?: T;
+ onPresentHandler?: (v: T) => void;
}
export interface ArgumentArgs<T> {
@@ -269,9 +270,6 @@ export class CommandGroup<GN extends keyof any, TG> {
}
printHelp(progName: string, parents: CommandGroup<any, any>[]) {
- const chain: CommandGroup<any, any>[] = Array.prototype.concat(parents, [
- this,
- ]);
let usageSpec = "";
for (let p of parents) {
usageSpec += (p.name ?? progName) + " ";
@@ -352,6 +350,7 @@ export class CommandGroup<GN extends keyof any, TG> {
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<GN extends keyof any, TG> {
}
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<GN extends keyof any, TG> {
}
}
+ 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<PN extends keyof any, T> {
name: N,
flagspec: string[],
args: OptionArgs<boolean> = {},
- ): Program<N, T & SubRecord<PN, N, boolean>> {
+ ): Program<PN, T & SubRecord<PN, N, boolean>> {
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<import("../http").HttpResponse> {
- 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<import("../http").HttpResponse> {
- 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<void> {
+ 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<T>(
}
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;
+ }
}
}
});