diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-08-17 01:54:01 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-08-17 01:54:01 +0200 |
commit | 5ff600fed792919938facfae07be3a73cb4bfb36 (patch) | |
tree | 8f355d4f27b983316b69a6e1f9d32104577f1cf4 | |
parent | 9e3a26ca70e77265f74527eb183c3f80b6531a97 (diff) |
start with an actual wallet cli
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/db.ts | 5 | ||||
-rw-r--r-- | src/headless/bank.ts | 102 | ||||
-rw-r--r-- | src/headless/helpers.ts | 229 | ||||
-rw-r--r-- | src/headless/taler-wallet-cli.ts | 1 | ||||
-rw-r--r-- | src/headless/taler-wallet-testing.ts | 155 | ||||
-rw-r--r-- | src/wallet.ts | 3 | ||||
-rw-r--r-- | yarn.lock | 8 |
8 files changed, 500 insertions, 5 deletions
diff --git a/package.json b/package.json index 6bdf9c9a5..e1992490a 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/urijs": "^1.19.3", "axios": "^0.19.0", "commander": "^2.20.0", - "idb-bridge": "^0.0.5", + "idb-bridge": "^0.0.6", "source-map-support": "^0.5.12", "urijs": "^1.18.10" } @@ -12,13 +12,17 @@ export function openTalerDb( onVersionChange: () => void, onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void, ): Promise<IDBDatabase> { + console.log("in openTalerDb"); return new Promise<IDBDatabase>((resolve, reject) => { + console.log("calling factory.open"); const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION); + console.log("after factory.open"); req.onerror = e => { console.log("taler database error", e); reject(e); }; req.onsuccess = e => { + console.log("in openTalerDb onsuccess"); req.result.onversionchange = (evt: IDBVersionChangeEvent) => { console.log( `handling live db version change from ${evt.oldVersion} to ${ @@ -31,6 +35,7 @@ export function openTalerDb( resolve(req.result); }; req.onupgradeneeded = e => { + console.log("in openTalerDb onupgradeneeded"); const db = req.result; console.log( `DB: upgrade needed: oldVersion=${e.oldVersion}, newVersion=${ 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 <http://www.gnu.org/licenses/> + */ + +/** + * 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<BankUser> { + 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 <http://www.gnu.org/licenses/> + */ + +/** + * 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<import("../http").HttpResponse> { + 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<import("../http").HttpResponse> { + 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<import("../http").HttpResponse> { + 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<Wallet> { + 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 <http://www.gnu.org/licenses/> + */ + +/** + * 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<CheckPaymentResponse> { + 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); + }); +} diff --git a/src/wallet.ts b/src/wallet.ts index ffdb60fa3..c8dd56bbd 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -3413,11 +3413,14 @@ export class Wallet { .deleteIf(Stores.exchanges, gcExchange) .finish(); + // FIXME: check if this is correct! const gcDenominations = (d: DenominationRecord, n: number) => { if (nowSec > getTalerStampSec(d.stampExpireDeposit)!) { + console.log("garbage-collecting denomination due to expiration"); return true; } if (activeExchanges.indexOf(d.exchangeBaseUrl) < 0) { + console.log("garbage-collecting denomination due to missing exchange"); return true; } return false; @@ -3240,10 +3240,10 @@ iconv-lite@^0.4.4, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -idb-bridge@^0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.5.tgz#fb26ddc3183229ae54f31c4b8709312b57735fed" - integrity sha512-ya5Hf5R6S0Pimeg6+8iL4MYR7duwywtZ2Dxm/HIdY4JIee9cITfuNIRRXOEQhxUxZdbYQeqjNYIRnK5bAQSUUw== +idb-bridge@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.6.tgz#efdf7e6fdb3deec14e4b84c70b0af72e78a52d4d" + integrity sha512-0IjViZiibJxHNKJw1D+trd+TS1wDRUUCTstz4ga6uc3Nje4qXOrVDaF56COcvOYywY9w6YCVtN52Am6+Ce85QQ== ieee754@^1.1.4: version "1.1.13" |