From 5ff600fed792919938facfae07be3a73cb4bfb36 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 17 Aug 2019 01:54:01 +0200 Subject: start with an actual wallet cli --- src/headless/bank.ts | 102 ++++++++++++++++ src/headless/helpers.ts | 229 +++++++++++++++++++++++++++++++++++ src/headless/taler-wallet-cli.ts | 1 + src/headless/taler-wallet-testing.ts | 155 ++++++++++++++++++++++++ 4 files changed, 487 insertions(+) create mode 100644 src/headless/bank.ts create mode 100644 src/headless/helpers.ts create mode 100644 src/headless/taler-wallet-testing.ts (limited to 'src/headless') diff --git a/src/headless/bank.ts b/src/headless/bank.ts new file mode 100644 index 000000000..0af9cb19a --- /dev/null +++ b/src/headless/bank.ts @@ -0,0 +1,102 @@ +/* + 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 + */ + +/** + * Helper functions to deal with the GNU Taler demo bank. + * + * Mostly useful for automated tests. + */ + +/** + * Imports. + */ +import Axios from "axios"; +import querystring = require("querystring"); +import URI = require("urijs"); + +export interface BankUser { + username: string; + password: string; +} + +function makeId(length: number): string { + let result = ""; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +export class Bank { + constructor(private bankBaseUrl: string) {} + + async createReserve( + bankUser: BankUser, + amount: string, + reservePub: string, + exchangePaytoUri: string, + ) { + const reqUrl = new URI("taler/withdraw") + .absoluteTo(this.bankBaseUrl) + .href(); + + const body = { + auth: { type: "basic" }, + username: bankUser, + amount, + reserve_pub: reservePub, + exchange_wire_detail: exchangePaytoUri, + }; + + 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"); + } + } + + async registerRandomUser(): Promise { + const reqUrl = new URI("register").absoluteTo(this.bankBaseUrl).href(); + const randId = makeId(8); + const bankUser: BankUser = { + username: `testuser-${randId}`, + password: `testpw-${randId}`, + }; + + const resp = await Axios({ + method: "post", + url: reqUrl, + data: querystring.stringify(bankUser), + responseType: "json", + }); + + if (resp.status != 200) { + throw Error("could not register bank user"); + } + return bankUser; + } +} diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts new file mode 100644 index 000000000..0ac7accb6 --- /dev/null +++ b/src/headless/helpers.ts @@ -0,0 +1,229 @@ +/* + 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 + */ + +/** + * Helpers to create headless wallets. + */ + +/** + * Imports. + */ +import { Wallet } from "../wallet"; +import { Notifier, Badge } from "../walletTypes"; +import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge"; +import { SynchronousCryptoWorkerFactory } from "../crypto/synchronousWorker"; +import { openTalerDb } from "../db"; +import Axios from "axios"; +import querystring = require("querystring"); +import { HttpRequestLibrary } from "../http"; +import * as amounts from "../amounts"; +import { Bank } from "./bank"; + +import fs = require("fs"); + +const enableTracing = false; + +class ConsoleNotifier implements Notifier { + notify(): void { + // nothing to do. + } +} + +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); + 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, + }; + } + + async postJson( + url: string, + body: any, + ): Promise { + enableTracing && console.log("making POST request to", url); + const resp = await Axios({ + method: "post", + url: url, + 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, + }; + } + + async postForm( + url: string, + form: any, + ): Promise { + enableTracing && console.log("making POST request to", url); + const resp = await Axios({ + method: "post", + url: url, + data: querystring.stringify(form), + responseType: "json", + }); + enableTracing && console.log("got response", resp.data); + enableTracing && console.log("resp type", typeof resp.data); + return { + responseJson: resp.data, + status: resp.status, + }; + } +} + +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; +} + +/** + * Get a wallet instance with default settings for node. + */ +export async function getDefaultNodeWallet( + args: DefaultNodeWalletArgs = {}, +): Promise { + const myNotifier = new ConsoleNotifier(); + + const myBadge = new ConsoleBadge(); + + const myBackend = new MemoryBackend(); + myBackend.enableTracing = false; + + const storagePath = args.persistentStoragePath; + if (storagePath) { + console.log(`using storage path ${storagePath}`); + + try { + const dbContentStr: string = fs.readFileSync(storagePath, { encoding: "utf-8" }); + const dbContent = JSON.parse(dbContentStr); + myBackend.importDump(dbContent); + console.log("imported wallet"); + } catch (e) { + console.log("could not read wallet file"); + } + + myBackend.afterCommitCallback = async () => { + console.log("in afterCommitCallback!"); + 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; + + const myHttpLib = new NodeHttpLib(); + + const myVersionChange = () => { + console.error("version change requested, should not happen"); + throw Error(); + }; + + const myUnsupportedUpgrade = () => { + console.error("unsupported database migration"); + throw Error(); + }; + + shimIndexedDB(myBridgeIdbFactory); + + console.log("opening taler DB"); + + const myDb = await openTalerDb( + myIdbFactory, + myVersionChange, + myUnsupportedUpgrade, + ); + + console.log("opened db"); + + return new Wallet( + myDb, + myHttpLib, + myBadge, + myNotifier, + new SynchronousCryptoWorkerFactory(), + ); + //const myWallet = new Wallet(myDb, myHttpLib, myBadge, myNotifier, new NodeCryptoWorkerFactory()); +} + +export async function withdrawTestBalance( + myWallet: Wallet, + amount: string = "TESTKUDOS:10", + bankBaseUrl: string = "https://bank.test.taler.net/", + exchangeBaseUrl: string = "https://exchange.test.taler.net/", +) { + const reserveResponse = await myWallet.createReserve({ + amount: amounts.parseOrThrow("TESTKUDOS:10.0"), + exchange: exchangeBaseUrl, + }); + + const bank = new Bank(bankBaseUrl); + + const bankUser = await bank.registerRandomUser(); + + console.log("bank user", bankUser); + + const exchangePaytoUri = await myWallet.getExchangePaytoUri( + exchangeBaseUrl, + ["x-taler-bank"], + ); + + await bank.createReserve( + bankUser, + amount, + reserveResponse.reservePub, + exchangePaytoUri, + ); + + await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub }); + + await myWallet.processReserve(reserveResponse.reservePub); +} diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 26f4521c5..9c4a84e3e 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -43,6 +43,7 @@ program const wallet = await getDefaultNodeWallet({ persistentStoragePath: walletDbPath, }); + console.log("got wallet"); const balance = await wallet.getBalances(); console.log(JSON.stringify(balance, undefined, 2)); process.exit(0); diff --git a/src/headless/taler-wallet-testing.ts b/src/headless/taler-wallet-testing.ts new file mode 100644 index 000000000..cb0cfeacc --- /dev/null +++ b/src/headless/taler-wallet-testing.ts @@ -0,0 +1,155 @@ +/* + 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 + */ + +/** + * Integration tests against real Taler bank/exchange/merchant deployments. + */ + +import { Wallet } from "../wallet"; +import * as amounts from "../amounts"; +import Axios from "axios"; +import URI = require("urijs"); + +import { CheckPaymentResponse } from "../talerTypes"; +import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers"; +import { Bank } from "./bank"; + +const enableTracing = false; + +class MerchantBackendConnection { + constructor( + public merchantBaseUrl: string, + public merchantInstance: string, + public apiKey: string, + ) {} + + async createOrder( + amount: string, + summary: string, + fulfillmentUrl: string, + ): Promise<{ orderId: string }> { + const reqUrl = new URI("order").absoluteTo(this.merchantBaseUrl).href(); + const orderReq = { + order: { + amount, + summary, + fulfillment_url: fulfillmentUrl, + instance: this.merchantInstance, + }, + }; + const resp = await Axios({ + method: "post", + url: reqUrl, + data: orderReq, + responseType: "json", + headers: { + Authorization: `ApiKey ${this.apiKey}`, + }, + }); + if (resp.status != 200) { + throw Error("failed to create bank reserve"); + } + const orderId = resp.data.order_id; + if (!orderId) { + throw Error("no order id in response"); + } + return { orderId }; + } + + async checkPayment(orderId: string): Promise { + const reqUrl = new URI("check-payment") + .absoluteTo(this.merchantBaseUrl) + .href(); + const resp = await Axios({ + method: "get", + url: reqUrl, + params: { order_id: orderId, instance: this.merchantInstance }, + responseType: "json", + headers: { + Authorization: `ApiKey ${this.apiKey}`, + }, + }); + if (resp.status != 200) { + throw Error("failed to check payment"); + } + return CheckPaymentResponse.checked(resp.data); + } +} + +export async function main() { + const exchangeBaseUrl = "https://exchange.test.taler.net/"; + const bankBaseUrl = "https://bank.test.taler.net/"; + + const myWallet = await getDefaultNodeWallet(); + + await withdrawTestBalance(myWallet); + + const balance = await myWallet.getBalances(); + + console.log(JSON.stringify(balance, null, 2)); + + const myMerchant = new MerchantBackendConnection( + "https://backend.test.taler.net/", + "default", + "sandbox", + ); + + const orderResp = await myMerchant.createOrder( + "TESTKUDOS:5", + "hello world", + "https://example.com/", + ); + + console.log("created order with orderId", orderResp.orderId); + + const paymentStatus = await myMerchant.checkPayment(orderResp.orderId); + + console.log("payment status", paymentStatus); + + const contractUrl = paymentStatus.contract_url; + if (!contractUrl) { + throw Error("no contract URL in payment response"); + } + + const proposalId = await myWallet.downloadProposal(contractUrl); + + console.log("proposal id", proposalId); + + const checkPayResult = await myWallet.checkPay(proposalId); + + console.log("check pay result", checkPayResult); + + const confirmPayResult = await myWallet.confirmPay(proposalId, undefined); + + console.log("confirmPayResult", confirmPayResult); + + const paymentStatus2 = await myMerchant.checkPayment(orderResp.orderId); + + console.log("payment status after wallet payment:", paymentStatus2); + + if (!paymentStatus2.paid) { + throw Error("payment did not succeed"); + } + + myWallet.stop(); +} + +if (require.main === module) { + main().catch(err => { + console.error("Failed with exception:"); + console.error(err); + }); +} -- cgit v1.2.3