From 01e83df471802d3253953b00672af0bc879403fe Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 24 Mar 2020 15:25:04 +0530 Subject: [PATCH] helpers for auditor integration test --- src/headless/taler-wallet-cli.ts | 136 ++++++++++++++++++++++--------- src/operations/pay.ts | 4 - src/operations/refresh.ts | 3 +- src/operations/withdraw.ts | 1 + src/types/dbTypes.ts | 6 +- src/types/talerTypes.ts | 14 ++++ src/util/logging.ts | 6 ++ src/wallet.ts | 103 ++++++++++++++++++----- 8 files changed, 206 insertions(+), 67 deletions(-) diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 9a21d2a1d..174953919 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -32,6 +32,7 @@ import { classifyTalerUri, TalerUriType } from "../util/taleruri"; import util = require("util"); import { Configuration } from "../util/talerconfig"; import { setDangerousTimetravel } from "../util/time"; +import { makeCodecForList, codecForString } from "../util/codec"; // Backwards compatibility with nodejs<0.11, where TextEncoder and TextDecoder // are not globals yet. @@ -118,7 +119,7 @@ const walletCli = clk help: "Command line interface for the GNU Taler wallet.", }) .maybeOption("walletDbFile", ["--wallet-db"], clk.STRING, { - help: "location of the wallet database file" + help: "location of the wallet database file", }) .maybeOption("timetravel", ["--timetravel"], clk.INT, { help: "modify system time by given offset in microseconds", @@ -172,8 +173,8 @@ walletCli .flag("json", ["--json"], { help: "Show raw JSON.", }) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const balance = await wallet.getBalances(); if (args.balance.json) { console.log(JSON.stringify(balance, undefined, 2)); @@ -195,8 +196,8 @@ walletCli .maybeOption("to", ["--to"], clk.STRING) .maybeOption("limit", ["--limit"], clk.STRING) .maybeOption("contEvt", ["--continue-with"], clk.STRING) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const history = await wallet.getHistory(); if (args.history.json) { console.log(JSON.stringify(history, undefined, 2)); @@ -216,8 +217,8 @@ walletCli walletCli .subcommand("", "pending", { help: "Show pending operations." }) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const pending = await wallet.getPendingOperations(); console.log(JSON.stringify(pending, undefined, 2)); }); @@ -234,8 +235,8 @@ walletCli help: "Run pending operations.", }) .flag("forceNow", ["-f", "--force-now"]) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { await wallet.runPending(args.runPendingOpt.forceNow); }); }); @@ -246,8 +247,8 @@ walletCli }) .requiredArgument("uri", clk.STRING) .flag("autoYes", ["-y", "--yes"]) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const uri: string = args.handleUri.uri; const uriType = classifyTalerUri(uri); switch (uriType) { @@ -294,9 +295,9 @@ exchangesCli .subcommand("exchangesListCmd", "list", { help: "List known exchanges.", }) - .action(async args => { + .action(async (args) => { console.log("Listing exchanges ..."); - await withWallet(args, async wallet => { + await withWallet(args, async (wallet) => { const exchanges = await wallet.getExchanges(); console.log("exchanges", exchanges); }); @@ -310,8 +311,8 @@ exchangesCli help: "Base URL of the exchange.", }) .flag("force", ["-f", "--force"]) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const res = await wallet.updateExchangeFromUrl( args.exchangesUpdateCmd.url, args.exchangesUpdateCmd.force, @@ -328,7 +329,7 @@ advancedCli .subcommand("decode", "decode", { help: "Decode base32-crockford.", }) - .action(args => { + .action((args) => { const enc = fs.readFileSync(0, "utf8"); fs.writeFileSync(1, decodeCrock(enc.trim())); }); @@ -338,8 +339,8 @@ advancedCli help: "Claim an order but don't pay yet.", }) .requiredArgument("url", clk.STRING) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const res = await wallet.preparePayForUri(args.payPrepare.url); switch (res.status) { case "error": @@ -365,18 +366,75 @@ advancedCli help: "Force a refresh on a coin.", }) .requiredArgument("coinPub", clk.STRING) - .action(async args => { - await withWallet(args, async wallet => { + .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 => { + .action(async (args) => { + await withWallet(args, async (wallet) => { const coins = await wallet.getCoins(); for (const coin of coins) { console.log(`coin ${coin.coinPub}`); @@ -395,8 +453,8 @@ advancedCli help: "Update reserve status.", }) .requiredArgument("reservePub", clk.STRING) - .action(async args => { - await withWallet(args, async wallet => { + .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)); }); @@ -407,8 +465,8 @@ advancedCli help: "Show the current reserve status.", }) .requiredArgument("reservePub", clk.STRING) - .action(async args => { - await withWallet(args, async wallet => { + .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)); }); @@ -421,7 +479,7 @@ const testCli = walletCli.subcommand("testingArgs", "testing", { testCli .subcommand("integrationtestBasic", "integrationtest-basic") .requiredArgument("cfgfile", clk.STRING) - .action(async args => { + .action(async (args) => { const cfgStr = fs.readFileSync(args.integrationtestBasic.cfgfile, "utf8"); const cfg = new Configuration(); cfg.loadFromString(cfgStr); @@ -429,7 +487,7 @@ testCli await runIntegrationTestBasic(cfg); } catch (e) { console.log("integration test failed"); - console.log(e) + console.log(e); process.exit(1); } process.exit(0); @@ -441,7 +499,7 @@ testCli .requiredOption("summary", ["-s", "--summary"], clk.STRING, { default: "Test Payment", }) - .action(async args => { + .action(async (args) => { const cmdArgs = args.testPayCmd; console.log("creating order"); const merchantBackend = new MerchantBackendConnection( @@ -462,7 +520,7 @@ testCli return; } console.log("taler pay URI:", talerPayUri); - await withWallet(args, async wallet => { + await withWallet(args, async (wallet) => { await doPay(wallet, talerPayUri, { alwaysYes: true }); }); }); @@ -489,7 +547,7 @@ testCli .requiredOption("spendAmount", ["-s", "--spend-amount"], clk.STRING, { default: "TESTKUDOS:4", }) - .action(async args => { + .action(async (args) => { applyVerbose(args.wallet.verbose); let cmdObj = args.integrationtestCmd; @@ -501,7 +559,7 @@ testCli exchangeBaseUrl: cmdObj.exchange, merchantApiKey: cmdObj.merchantApiKey, merchantBaseUrl: cmdObj.merchant, - }).catch(err => { + }).catch((err) => { console.error("Integration test failed with exception:"); console.error(err); process.exit(1); @@ -520,7 +578,7 @@ testCli .requiredOption("amount", ["-a", "--amount"], clk.STRING, { default: "TESTKUDOS:10", }) - .action(async args => { + .action(async (args) => { const merchantBackend = new MerchantBackendConnection( "https://backend.test.taler.net/", "sandbox", @@ -539,7 +597,7 @@ testCli .requiredOption("bank", ["-b", "--bank"], clk.STRING, { default: "https://bank.test.taler.net/", }) - .action(async args => { + .action(async (args) => { const b = new Bank(args.genWithdrawUri.bank); const user = await b.registerRandomUser(); const url = await b.generateWithdrawUri(user, args.genWithdrawUri.amount); @@ -559,7 +617,7 @@ testCli .requiredOption("summary", ["-s", "--summary"], clk.STRING, { default: "Test Payment (for refund)", }) - .action(async args => { + .action(async (args) => { const cmdArgs = args.genRefundUri; const merchantBackend = new MerchantBackendConnection( "https://backend.test.taler.net/", @@ -578,7 +636,7 @@ testCli process.exit(1); return; } - await withWallet(args, async wallet => { + await withWallet(args, async (wallet) => { await doPay(wallet, talerPayUri, { alwaysYes: true }); }); const refundUri = await merchantBackend.refund( @@ -611,7 +669,7 @@ testCli .requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, { default: "sandbox", }) - .action(async args => { + .action(async (args) => { const cmdArgs = args.genPayUri; console.log("creating order"); const merchantBackend = new MerchantBackendConnection( @@ -669,8 +727,8 @@ testCli default: "https://bank.test.taler.net/", help: "Bank base URL", }) - .action(async args => { - await withWallet(args, async wallet => { + .action(async (args) => { + await withWallet(args, async (wallet) => { await withdrawTestBalance( wallet, args.withdrawArgs.amount, diff --git a/src/operations/pay.ts b/src/operations/pay.ts index b8a63cb11..9a8017e4e 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -318,10 +318,6 @@ async function getCoinsForPayment( .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl) .toArray(); - const denoms = await ws.db - .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) - .toArray(); - if (!coins || coins.length === 0) { continue; } diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts index 092d9f154..c04b79278 100644 --- a/src/operations/refresh.ts +++ b/src/operations/refresh.ts @@ -412,7 +412,8 @@ async function refreshReveal( coinSource: { type: CoinSourceType.Refresh, oldCoinPub: refreshSession.meltCoinPub, - } + }, + suspended: false, }; coins.push(coin); diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 09d912bcc..37993023e 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -240,6 +240,7 @@ async function processPlanchet( reservePub: planchet.reservePub, withdrawSessionId: withdrawalSessionId, }, + suspended: false, }; let withdrawSessionFinished = false; diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index f28426ac9..5a5ac7c3d 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -674,11 +674,9 @@ export interface CoinRecord { exchangeBaseUrl: string; /** - * We have withdrawn the coin, but it's not accepted by the exchange anymore. - * We have to tell an auditor and wait for compensation or for the exchange - * to fix it. + * The coin is currently suspended, and will not be used for payments. */ - suspended?: boolean; + suspended: boolean; /** * Blinding key used when withdrawing the coin. diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts index 569b93120..e5be32abe 100644 --- a/src/types/talerTypes.ts +++ b/src/types/talerTypes.ts @@ -759,6 +759,20 @@ export class WithdrawResponse { ev_sig: string; } +export interface CoinDumpJson { + coins: Array<{ + denom_pub: string; + denom_pub_hash: string; + denom_value: string; + coin_pub: string; + exchange_base_url: string; + remaining_value: string; + refresh_parent_coin_pub: string | undefined; + withdrawal_reserve_pub: string | undefined; + coin_suspended: boolean; + }>; +} + export type AmountString = string; export type Base32String = string; export type EddsaSignatureString = string; diff --git a/src/util/logging.ts b/src/util/logging.ts index 309d1593b..4560105f4 100644 --- a/src/util/logging.ts +++ b/src/util/logging.ts @@ -19,6 +19,12 @@ export class Logger { info(message: string, ...args: any[]) { console.log(`${new Date().toISOString()} ${this.tag} INFO ` + message, ...args); } + warn(message: string, ...args: any[]) { + console.log(`${new Date().toISOString()} ${this.tag} WARN ` + message, ...args); + } + error(message: string, ...args: any[]) { + console.log(`${new Date().toISOString()} ${this.tag} ERROR ` + message, ...args); + } trace(message: any, ...args: any[]) { console.log(`${new Date().toISOString()} ${this.tag} TRACE ` + message, ...args) } diff --git a/src/wallet.ts b/src/wallet.ts index 6245941a1..df83eec84 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -53,8 +53,9 @@ import { ReserveRecord, Stores, ReserveRecordStatus, + CoinSourceType, } from "./types/dbTypes"; -import { MerchantRefundPermission } from "./types/talerTypes"; +import { MerchantRefundPermission, CoinDumpJson } from "./types/talerTypes"; import { BenchmarkResult, ConfirmPayResult, @@ -238,7 +239,10 @@ export class Wallet { await this.processOnePendingOperation(p, forceNow); } catch (e) { if (e instanceof OperationFailedAndReportedError) { - console.error("Operation failed:", JSON.stringify(e.operationError, undefined, 2)); + console.error( + "Operation failed:", + JSON.stringify(e.operationError, undefined, 2), + ); } else { console.error(e); } @@ -254,7 +258,7 @@ export class Wallet { public async runUntilDone(): Promise { const p = new Promise((resolve, reject) => { // Run this asynchronously - this.addNotificationListener(n => { + this.addNotificationListener((n) => { if ( n.type === NotificationType.WaitingForRetry && n.numGivingLiveness == 0 @@ -263,7 +267,7 @@ export class Wallet { resolve(); } }); - this.runRetryLoop().catch(e => { + this.runRetryLoop().catch((e) => { console.log("exception in wallet retry loop"); reject(e); }); @@ -279,7 +283,7 @@ export class Wallet { public async runUntilDoneAndStop(): Promise { const p = new Promise((resolve, reject) => { // Run this asynchronously - this.addNotificationListener(n => { + this.addNotificationListener((n) => { if ( n.type === NotificationType.WaitingForRetry && n.numGivingLiveness == 0 @@ -288,7 +292,7 @@ export class Wallet { this.stop(); } }); - this.runRetryLoop().catch(e => { + this.runRetryLoop().catch((e) => { console.log("exception in wallet retry loop"); reject(e); }); @@ -371,9 +375,9 @@ export class Wallet { async fillDefaults() { await this.db.runWithWriteTransaction( [Stores.config, Stores.currencies], - async tx => { + async (tx) => { let applied = false; - await tx.iter(Stores.config).forEach(x => { + await tx.iter(Stores.config).forEach((x) => { if (x.key == "currencyDefaultsApplied" && x.value == true) { applied = true; } @@ -506,7 +510,7 @@ export class Wallet { try { const refreshGroupId = await this.db.runWithWriteTransaction( [Stores.refreshGroups], - async tx => { + async (tx) => { return await createRefreshGroup( tx, [{ coinPub: oldCoinPub }], @@ -573,13 +577,13 @@ export class Wallet { async getReserves(exchangeBaseUrl: string): Promise { return await this.db .iter(Stores.reserves) - .filter(r => r.exchangeBaseUrl === exchangeBaseUrl); + .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl); } async getCoinsForExchange(exchangeBaseUrl: string): Promise { return await this.db .iter(Stores.coins) - .filter(c => c.exchangeBaseUrl === exchangeBaseUrl); + .filter((c) => c.exchangeBaseUrl === exchangeBaseUrl); } async getCoins(): Promise { @@ -598,22 +602,22 @@ export class Wallet { async getSenderWireInfos(): Promise { const m: { [url: string]: Set } = {}; - await this.db.iter(Stores.exchanges).forEach(x => { + await this.db.iter(Stores.exchanges).forEach((x) => { const wi = x.wireInfo; if (!wi) { return; } const s = (m[x.baseUrl] = m[x.baseUrl] || new Set()); - Object.keys(wi.feesForType).map(k => s.add(k)); + Object.keys(wi.feesForType).map((k) => s.add(k)); }); const exchangeWireTypes: { [url: string]: string[] } = {}; - Object.keys(m).map(e => { + Object.keys(m).map((e) => { exchangeWireTypes[e] = Array.from(m[e]); }); const senderWiresSet: Set = new Set(); - await this.db.iter(Stores.senderWires).forEach(x => { + await this.db.iter(Stores.senderWires).forEach((x) => { senderWiresSet.add(x.paytoUri); }); @@ -735,20 +739,20 @@ export class Wallet { } const refundsDoneAmounts = Object.values( purchase.refundState.refundsDone, - ).map(x => Amounts.parseOrThrow(x.perm.refund_amount)); + ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const refundsPendingAmounts = Object.values( purchase.refundState.refundsPending, - ).map(x => Amounts.parseOrThrow(x.perm.refund_amount)); + ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const totalRefundAmount = Amounts.sum([ ...refundsDoneAmounts, ...refundsPendingAmounts, ]).amount; const refundsDoneFees = Object.values( purchase.refundState.refundsDone, - ).map(x => Amounts.parseOrThrow(x.perm.refund_amount)); + ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const refundsPendingFees = Object.values( purchase.refundState.refundsPending, - ).map(x => Amounts.parseOrThrow(x.perm.refund_amount)); + ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const totalRefundFees = Amounts.sum([ ...refundsDoneFees, ...refundsPendingFees, @@ -765,4 +769,65 @@ export class Wallet { benchmarkCrypto(repetitions: number): Promise { return this.ws.cryptoApi.benchmark(repetitions); } + + async setCoinSuspended(coinPub: string, suspended: boolean): Promise { + await this.db.runWithWriteTransaction([Stores.coins], async (tx) => { + const c = await tx.get(Stores.coins, coinPub); + if (!c) { + logger.warn(`coin ${coinPub} not found, won't suspend`); + return; + } + c.suspended = suspended; + await tx.put(Stores.coins, c); + }); + } + + /** + * Dump the public information of coins we have in an easy-to-process format. + */ + async dumpCoins(): Promise { + const coins = await this.db.iter(Stores.coins).toArray(); + const coinsJson: CoinDumpJson = { coins: [] }; + for (const c of coins) { + const denom = await this.db.get(Stores.denominations, [ + c.exchangeBaseUrl, + c.denomPub, + ]); + if (!denom) { + console.error("no denom session found for coin"); + continue; + } + const cs = c.coinSource; + let refreshParentCoinPub: string | undefined; + if (cs.type == CoinSourceType.Refresh) { + refreshParentCoinPub = cs.oldCoinPub; + } + let withdrawalReservePub: string | undefined; + if (cs.type == CoinSourceType.Withdraw) { + const ws = await this.db.get( + Stores.withdrawalSession, + cs.withdrawSessionId, + ); + if (!ws) { + console.error("no withdrawal session found for coin"); + continue; + } + if (ws.source.type == "reserve") { + withdrawalReservePub = ws.source.reservePub; + } + } + coinsJson.coins.push({ + coin_pub: c.coinPub, + denom_pub: c.denomPub, + denom_pub_hash: c.denomPubHash, + denom_value: Amounts.toString(denom.value), + exchange_base_url: c.exchangeBaseUrl, + refresh_parent_coin_pub: refreshParentCoinPub, + remaining_value: Amounts.toString(c.currentAmount), + withdrawal_reserve_pub: withdrawalReservePub, + coin_suspended: c.suspended, + }); + } + return coinsJson; + } }