diff options
Diffstat (limited to 'packages/taler-wallet-cli/src/index.ts')
-rw-r--r-- | packages/taler-wallet-cli/src/index.ts | 1685 |
1 files changed, 1184 insertions, 501 deletions
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 142e98e7c..b915de538 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -17,49 +17,58 @@ /** * Imports. */ -import os from "os"; -import fs from "fs"; -import path from "path"; -import { deepStrictEqual } from "assert"; -// Polyfill for encoding which isn't present globally in older nodejs versions -import { TextEncoder, TextDecoder } from "util"; -// @ts-ignore -global.TextEncoder = TextEncoder; -// @ts-ignore -global.TextDecoder = TextDecoder; -import * as clk from "./clk.js"; -import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { - PreparePayResultType, - setDangerousTimetravel, - classifyTalerUri, - TalerUriType, - RecoveryMergeStrategy, - Amounts, + AbsoluteTime, addPaytoQueryParams, + AgeRestriction, + AmountString, codecForList, codecForString, + CoreApiResponse, + Duration, + encodeCrock, + getErrorDetailFromException, + getRandomBytes, + j2s, Logger, - Configuration, - decodeCrock, - rsaBlind, + NotificationType, + parsePaytoUri, + parseTalerUri, + PreparePayResultType, + sampleWalletCoreTransactions, + setDangerousTimetravel, + setGlobalLogLevelFromString, + summarizeTalerErrorDetail, + TalerUriAction, + TransactionIdStr, + WalletNotification, } from "@gnu-taler/taler-util"; +import { clk } from "@gnu-taler/taler-util/clk"; +import { + getenv, + pathHomedir, + processExit, + readFile, + readlinePrompt, + setUnhandledRejectionHandler, +} from "@gnu-taler/taler-util/compat"; +import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; +import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc"; import { - NodeHttpLib, - getDefaultNodeWallet, - OperationFailedAndReportedError, - OperationFailedError, - NodeThreadCryptoWorkerFactory, - CryptoApi, - walletCoreDebugFlags, + AccessStats, + createNativeWalletHost2, + nativeCrypto, + Wallet, WalletApiOperation, WalletCoreApiClient, - Wallet, } from "@gnu-taler/taler-wallet-core"; -import { lintExchangeDeployment } from "./lint.js"; -import { runBench1 } from "./bench1.js"; -import { runEnv1 } from "./env1.js"; -import { GlobalTestState, runTestWithState } from "./harness/harness.js"; +import { + createRemoteWallet, + getClientFromRemoteWallet, + makeNotificationWaiter, +} from "@gnu-taler/taler-wallet-core/remote"; + +import * as fs from "node:fs"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -70,7 +79,19 @@ export { const logger = new Logger("taler-wallet-cli.ts"); -const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json"; +let observabilityEventFile: string | undefined = undefined; + +const EXIT_EXCEPTION = 4; +const EXIT_API_ERROR = 5; + +setUnhandledRejectionHandler((error: any) => { + logger.error("unhandledRejection", error.message); + logger.error("stack", error.stack); + processExit(1); +}); + +const defaultWalletDbPath = pathHomedir() + "/" + ".talerwalletdb.sqlite3"; +const defaultWalletCoreSocket = pathHomedir() + "/" + ".wallet-core.sock"; function assertUnreachable(x: never): never { throw new Error("Didn't expect to get here"); @@ -87,7 +108,7 @@ async function doPay( if (result.status === PreparePayResultType.InsufficientBalance) { console.log("contract", result.contractTerms); console.error("insufficient balance"); - process.exit(1); + processExit(1); return; } if (result.status === PreparePayResultType.AlreadyConfirmed) { @@ -96,8 +117,7 @@ async function doPay( } else { console.log("payment already in progress"); } - - process.exit(0); + processExit(0); return; } if (result.status === "payment-possible") { @@ -139,11 +159,11 @@ function applyVerbose(verbose: boolean): void { // TODO } +declare const __VERSION__: string; +declare const __GIT_HASH__: string; function printVersion(): void { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const info = require("../package.json"); - console.log(`${info.version}`); - process.exit(0); + console.log(`${__VERSION__} ${__GIT_HASH__}`); + processExit(0); } export const walletCli = clk @@ -151,7 +171,10 @@ export 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("walletConnection", ["--wallet-connection"], clk.STRING, { + help: "Connect to an RPC wallet", }) .maybeOption("timetravel", ["--timetravel"], clk.INT, { help: "modify system time by given offset in microseconds", @@ -161,61 +184,178 @@ export const walletCli = clk setDangerousTimetravel(x / 1000); }, }) + .maybeOption("cryptoWorker", ["--crypto-worker"], clk.STRING, { + help: "Override crypto worker implementation type.", + }) + .maybeOption("log", ["-L", "--log"], clk.STRING, { + help: "configure log level (NONE, ..., TRACE)", + onPresentHandler: (x) => { + setGlobalLogLevelFromString(x); + }, + }) .maybeOption("inhibit", ["--inhibit"], clk.STRING, { - help: - "Inhibit running certain operations, useful for debugging and testing.", + help: "Inhibit running certain operations, useful for debugging and testing.", }) .flag("noThrottle", ["--no-throttle"], { help: "Don't do any request throttling.", }) + .flag("noHttp", ["--no-http"], { + help: "Allow unsafe http connections.", + }) .flag("version", ["-v", "--version"], { onPresentHandler: printVersion, }) .flag("verbose", ["-V", "--verbose"], { help: "Enable verbose output.", + }) + .flag("skipDefaults", ["--skip-defaults"], { + help: "Skip configuring default exchanges.", }); type WalletCliArgsType = clk.GetArgType<typeof walletCli>; -async function withWallet<T>( +function checkEnvFlag(name: string): boolean { + const val = getenv(name); + if (val == "1") { + return true; + } + return false; +} + +export interface WalletContext { + /** + * High-level client for making API requests to wallet-core. + */ + client: WalletCoreApiClient; + + /** + * Low-level interface for making API requests to wallet-core. + */ + makeCoreApiRequest( + operation: string, + payload: unknown, + ): Promise<CoreApiResponse>; + + /** + * Return a promise that resolves after the wallet has emitted a notification + * that meets the criteria of the "cond" predicate. + */ + waitForNotificationCond<T>( + cond: (n: WalletNotification) => T | false | undefined, + ): Promise<T>; +} + +interface CreateWalletResult { + wallet: Wallet; + getStats: () => AccessStats; +} + +async function createLocalWallet( walletCliArgs: WalletCliArgsType, - f: (w: { client: WalletCoreApiClient; ws: Wallet }) => Promise<T>, -): Promise<T> { + notificationHandler?: (n: WalletNotification) => void, + noInit?: boolean, +): Promise<CreateWalletResult> { const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; - const myHttpLib = new NodeHttpLib(); - if (walletCliArgs.wallet.noThrottle) { - myHttpLib.setThrottling(false); - } - const wallet = await getDefaultNodeWallet({ - persistentStoragePath: dbPath, + const myHttpLib = createPlatformHttpLib({ + enableThrottling: walletCliArgs.wallet.noThrottle ? false : true, + requireTls: walletCliArgs.wallet.noHttp, + }); + const wh = await createNativeWalletHost2({ + persistentStoragePath: dbPath !== ":memory:" ? dbPath : undefined, httpLib: myHttpLib, + notifyHandler: (n) => { + logger.info(`wallet notification: ${j2s(n)}`); + if (notificationHandler) { + notificationHandler(n); + } + }, + cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any, }); + applyVerbose(walletCliArgs.wallet.verbose); + const res = { wallet: wh.wallet, getStats: wh.getDbStats }; + + if (noInit) { + return res; + } try { - const w = { - ws: wallet, - client: wallet.client, - }; - await wallet.handleCoreApiRequest("initWallet", "native-init", {}); - const ret = await f(w); - return ret; + await wh.wallet.handleCoreApiRequest("initWallet", "native-init", { + config: { + features: {}, + testing: { + devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), + denomselAllowLate: checkEnvFlag( + "TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE", + ), + emitObservabilityEvents: observabilityEventFile != null, + skipDefaults: walletCliArgs.wallet.skipDefaults, + }, + }, + }); + return res; } catch (e) { - if ( - e instanceof OperationFailedAndReportedError || - e instanceof OperationFailedError - ) { - console.error("Operation failed: " + e.message); - console.error( - "Error details:", - JSON.stringify(e.operationError, undefined, 2), - ); - } else { - console.error("caught unhandled exception (bug?):", e); + const ed = getErrorDetailFromException(e); + console.error("Operation failed: " + summarizeTalerErrorDetail(ed)); + console.error("Error details:", JSON.stringify(ed, undefined, 2)); + processExit(1); + } +} + +function writeObservabilityLog(notif: WalletNotification): void { + if (observabilityEventFile) { + switch (notif.type) { + case NotificationType.RequestObservabilityEvent: + case NotificationType.TaskObservabilityEvent: + fs.appendFileSync(observabilityEventFile, JSON.stringify(notif) + "\n"); + break; } - process.exit(1); - } finally { - logger.info("operation with wallet finished, stopping"); - wallet.stop(); + } +} + +async function withWallet<T>( + walletCliArgs: WalletCliArgsType, + f: (ctx: WalletContext) => Promise<T>, +): Promise<T> { + const waiter = makeNotificationWaiter(); + + const onNotif = (notif: WalletNotification) => { + waiter.notify(notif); + writeObservabilityLog(notif); + }; + + if (walletCliArgs.wallet.walletConnection) { + logger.info("creating remote wallet"); + const w = await createRemoteWallet({ + name: "wallet", + notificationHandler: onNotif, + socketFilename: walletCliArgs.wallet.walletConnection, + }); + const ctx: WalletContext = { + makeCoreApiRequest(operation, payload) { + return w.makeCoreApiRequest(operation, payload); + }, + client: getClientFromRemoteWallet(w), + waitForNotificationCond: waiter.waitForNotificationCond, + }; + const res = await f(ctx); + w.close(); + return res; + } else { + const wh = await createLocalWallet(walletCliArgs, onNotif); + const ctx: WalletContext = { + client: wh.wallet.client, + waitForNotificationCond: waiter.waitForNotificationCond, + makeCoreApiRequest(operation, payload) { + return wh.wallet.handleCoreApiRequest(operation, "my-req", payload); + }, + }; + const result = await f(ctx); + await wh.wallet.client.call(WalletApiOperation.Shutdown, {}); + if (process.env.TALER_WALLET_DBSTATS) { + console.log("database stats:"); + console.log(j2s(wh.getStats())); + } + return result; } } @@ -238,79 +378,185 @@ walletCli .subcommand("api", "api", { help: "Call the wallet-core API directly." }) .requiredArgument("operation", clk.STRING) .requiredArgument("request", clk.STRING) + .flag("expectSuccess", ["--expect-success"], { + help: "Exit with non-zero status code when request fails instead of returning error JSON.", + }) .action(async (args) => { await withWallet(args, async (wallet) => { let requestJson; + logger.info(`handling 'api' request (${args.api.operation})`); + const jsonContent = args.api.request.startsWith("@") + ? readFile(args.api.request.substring(1)) + : args.api.request; try { - requestJson = JSON.parse(args.api.request); + requestJson = JSON.parse(jsonContent); } catch (e) { console.error("Invalid JSON"); - process.exit(1); + processExit(1); + } + try { + const resp = await wallet.makeCoreApiRequest( + args.api.operation, + requestJson, + ); + console.log(JSON.stringify(resp, undefined, 2)); + if (resp.type === "error") { + if (args.api.expectSuccess) { + processExit(EXIT_API_ERROR); + } + } + } catch (e) { + logger.error(`Got exception while handling API request ${e}`); + processExit(EXIT_EXCEPTION); } - const resp = await wallet.ws.handleCoreApiRequest( - args.api.operation, - "reqid-1", - requestJson, - ); - console.log(JSON.stringify(resp, undefined, 2)); }); + logger.info("finished handling API request"); }); -walletCli - .subcommand("", "pending", { help: "Show pending operations." }) +const transactionsCli = walletCli + .subcommand("transactions", "transactions", { help: "Manage transactions." }) + .maybeOption("currency", ["--currency"], clk.STRING, { + help: "Filter by currency.", + }) + .maybeOption("search", ["--search"], clk.STRING, { + help: "Filter by search string", + }) + .flag("includeRefreshes", ["--include-refreshes"]); + +// Default action +transactionsCli.action(async (args) => { + await withWallet(args, async (wallet) => { + const pending = await wallet.client.call( + WalletApiOperation.GetTransactions, + { + currency: args.transactions.currency, + search: args.transactions.search, + includeRefreshes: args.transactions.includeRefreshes, + sort: "stable-ascending", + }, + ); + console.log(JSON.stringify(pending, undefined, 2)); + }); +}); + +transactionsCli + .subcommand("deleteTransaction", "delete", { + help: "Permanently delete a transaction from the transaction list.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) .action(async (args) => { await withWallet(args, async (wallet) => { - const pending = await wallet.client.call( - WalletApiOperation.GetPendingOperations, - {}, - ); - console.log(JSON.stringify(pending, undefined, 2)); + await wallet.client.call(WalletApiOperation.DeleteTransaction, { + transactionId: args.deleteTransaction.transactionId as TransactionIdStr, + }); }); }); -walletCli - .subcommand("transactions", "transactions", { help: "Show transactions." }) - .maybeOption("currency", ["--currency"], clk.STRING) - .maybeOption("search", ["--search"], clk.STRING) +transactionsCli + .subcommand("suspendTransaction", "suspend", { + help: "Suspend a transaction.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to suspend.", + }) .action(async (args) => { await withWallet(args, async (wallet) => { - const pending = await wallet.client.call( - WalletApiOperation.GetTransactions, + await wallet.client.call(WalletApiOperation.SuspendTransaction, { + transactionId: args.suspendTransaction + .transactionId as TransactionIdStr, + }); + }); + }); + +transactionsCli + .subcommand("fail", "fail", { + help: "Fail a transaction (when it can't be aborted).", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to fail.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.FailTransaction, { + transactionId: args.fail.transactionId as TransactionIdStr, + }); + }); + }); + +transactionsCli + .subcommand("resumeTransaction", "resume", { + help: "Resume a transaction.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to suspend.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.ResumeTransaction, { + transactionId: args.resumeTransaction.transactionId as TransactionIdStr, + }); + }); + }); + +transactionsCli + .subcommand("lookup", "lookup", { + help: "Look up a single transaction based on the transaction identifier.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const tx = await wallet.client.call( + WalletApiOperation.GetTransactionById, { - currency: args.transactions.currency, - search: args.transactions.search, + transactionId: args.lookup.transactionId, }, ); - console.log(JSON.stringify(pending, undefined, 2)); + console.log(j2s(tx)); }); }); -async function asyncSleep(milliSeconds: number): Promise<void> { - return new Promise<void>((resolve, reject) => { - setTimeout(() => resolve(), milliSeconds); +transactionsCli + .subcommand("abortTransaction", "abort", { + help: "Abort a transaction.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.AbortTransaction, { + transactionId: args.abortTransaction.transactionId as TransactionIdStr, + }); + }); }); -} walletCli - .subcommand("runPendingOpt", "run-pending", { - help: "Run pending operations.", + .subcommand("version", "version", { + help: "Show version details.", }) - .flag("forceNow", ["-f", "--force-now"]) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.ws.runPending(args.runPendingOpt.forceNow); + const versionInfo = await wallet.client.call( + WalletApiOperation.GetVersion, + {}, + ); + console.log(j2s(versionInfo)); }); }); -walletCli - .subcommand("retryTransaction", "retry-transaction", { +transactionsCli + .subcommand("retryTransaction", "retry", { help: "Retry a transaction.", }) .requiredArgument("transactionId", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { await wallet.client.call(WalletApiOperation.RetryTransaction, { - transactionId: args.retryTransaction.transactionId, + transactionId: args.retryTransaction.transactionId as TransactionIdStr, }); }); }); @@ -319,29 +565,76 @@ walletCli .subcommand("finishPendingOpt", "run-until-done", { help: "Run until no more work is left.", }) - .maybeOption("maxRetries", ["--max-retries"], clk.INT) .action(async (args) => { + await withWallet(args, async (ctx) => { + await ctx.client.call(WalletApiOperation.TestingWaitTasksDone, {}); + }); + }); + +const withdrawCli = walletCli.subcommand("withdraw", "withdraw", { + help: "Withdraw with a taler://withdraw/ URI", +}); + +withdrawCli + .subcommand("withdrawCheckUri", "check-uri") + .requiredArgument("uri", clk.STRING) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .action(async (args) => { + const uri = args.withdrawCheckUri.uri; + const restrictAge = args.withdrawCheckUri.restrictAge; + console.log(`age restriction requested (${restrictAge})`); await withWallet(args, async (wallet) => { - await wallet.ws.runTaskLoop({ - maxRetries: args.finishPendingOpt.maxRetries, - stopWhenDone: true, - }); - wallet.ws.stop(); + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: uri, + restrictAge, + }, + ); + console.log("withdrawInfo", withdrawInfo); }); }); -walletCli - .subcommand("deleteTransaction", "delete-transaction", { - help: "Permanently delete a transaction from the transaction list.", - }) - .requiredArgument("transactionId", clk.STRING, { - help: "Identifier of the transaction to delete", - }) +withdrawCli + .subcommand("withdrawCheckAmount", "check-amount") + .requiredArgument("exchange", clk.STRING) + .requiredArgument("amount", clk.AMOUNT) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .action(async (args) => { + const restrictAge = args.withdrawCheckAmount.restrictAge; + console.log(`age restriction requested (${restrictAge})`); await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.DeleteTransaction, { - transactionId: args.deleteTransaction.transactionId, - }); + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: args.withdrawCheckAmount.amount, + exchangeBaseUrl: args.withdrawCheckAmount.exchange, + restrictAge, + }, + ); + console.log("withdrawInfo", withdrawInfo); + }); + }); + +withdrawCli + .subcommand("withdrawAcceptUri", "accept-uri") + .requiredArgument("uri", clk.STRING) + .requiredOption("exchange", ["--exchange"], clk.STRING) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .action(async (args) => { + const uri = args.withdrawAcceptUri.uri; + const restrictAge = args.withdrawAcceptUri.restrictAge; + console.log(`age restriction requested (${restrictAge})`); + await withWallet(args, async (wallet) => { + const res = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: args.withdrawAcceptUri.exchange, + talerWithdrawUri: uri, + restrictAge, + }, + ); + console.log(j2s(res)); }); }); @@ -349,69 +642,125 @@ walletCli .subcommand("handleUri", "handle-uri", { help: "Handle a taler:// URI.", }) - .requiredArgument("uri", clk.STRING) + .maybeArgument("uri", clk.STRING) + .maybeOption("withdrawalExchange", ["--withdrawal-exchange"], clk.STRING, { + help: "Exchange to use for withdrawal operations.", + }) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .flag("autoYes", ["-y", "--yes"]) .action(async (args) => { await withWallet(args, async (wallet) => { - const uri: string = args.handleUri.uri; - const uriType = classifyTalerUri(uri); - switch (uriType) { - case TalerUriType.TalerPay: + let uri; + if (args.handleUri.uri) { + uri = args.handleUri.uri; + } else { + uri = await readlinePrompt("Taler URI: "); + } + const parsedTalerUri = parseTalerUri(uri); + if (!parsedTalerUri) { + throw Error("invalid taler URI"); + } + switch (parsedTalerUri.type) { + case TalerUriAction.Pay: await doPay(wallet.client, uri, { alwaysYes: args.handleUri.autoYes, }); break; - case TalerUriType.TalerTip: - { - const res = await wallet.client.call( - WalletApiOperation.PrepareTip, - { - talerTipUri: uri, - }, - ); - console.log("tip status", res); - await wallet.client.call(WalletApiOperation.AcceptTip, { - walletTipId: res.walletTipId, - }); - } - break; - case TalerUriType.TalerRefund: - await wallet.client.call(WalletApiOperation.ApplyRefund, { + case TalerUriAction.Refund: + await wallet.client.call(WalletApiOperation.StartRefundQueryForUri, { talerRefundUri: uri, }); break; - case TalerUriType.TalerWithdraw: - { - const withdrawInfo = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForUri, - { - talerWithdrawUri: uri, - }, - ); - console.log("withdrawInfo", withdrawInfo); - const selectedExchange = withdrawInfo.defaultExchangeBaseUrl; - if (!selectedExchange) { - console.error("no suggested exchange!"); - process.exit(1); - return; - } - const res = await wallet.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: selectedExchange, - talerWithdrawUri: uri, - }, + case TalerUriAction.Withdraw: { + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: uri, + restrictAge: args.handleUri.restrictAge, + }, + ); + console.log("withdrawInfo", withdrawInfo); + const selectedExchange = + args.handleUri.withdrawalExchange ?? + withdrawInfo.defaultExchangeBaseUrl; + if (!selectedExchange) { + console.error( + "no exchange specified for withdrawal (and no exchange suggested by the bank)", ); + processExit(1); + return; } + const res = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: selectedExchange, + talerWithdrawUri: uri, + }, + ); + console.log("accept withdrawal response", res); + break; + } + case TalerUriAction.DevExperiment: { + await wallet.client.call(WalletApiOperation.ApplyDevExperiment, { + devExperimentUri: uri, + }); break; + } default: - console.log(`URI type (${uriType}) not handled`); + console.log(`URI type (${parsedTalerUri.type}) not handled`); break; } return; }); }); +withdrawCli + .subcommand("withdrawManually", "manual", { + help: "Withdraw manually from an exchange.", + }) + .requiredOption("exchange", ["--exchange"], clk.STRING, { + help: "Base URL of the exchange.", + }) + .requiredOption("amount", ["--amount"], clk.AMOUNT, { + help: "Amount to withdraw", + }) + .maybeOption("forcedReservePriv", ["--forced-reserve-priv"], clk.STRING, {}) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const exchangeBaseUrl = args.withdrawManually.exchange; + const amount = args.withdrawManually.amount; + const d = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: args.withdrawManually.amount, + exchangeBaseUrl: exchangeBaseUrl, + }, + ); + const acct = d.withdrawalAccountsList[0]; + if (!acct) { + console.log("exchange has no accounts"); + return; + } + const resp = await wallet.client.call( + WalletApiOperation.AcceptManualWithdrawal, + { + amount, + exchangeBaseUrl, + restrictAge: parseInt(String(args.withdrawManually.restrictAge), 10), + forceReservePriv: args.withdrawManually.forcedReservePriv, + }, + ); + const reservePub = resp.reservePub; + const completePaytoUri = addPaytoQueryParams(acct.paytoUri, { + amount: args.withdrawManually.amount, + message: `Taler top-up ${reservePub}`, + }); + console.log("Created reserve", reservePub); + console.log("Payto URI", completePaytoUri); + }); + }); + const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", { help: "Manage exchanges.", }); @@ -441,14 +790,33 @@ exchangesCli .flag("force", ["-f", "--force"]) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.AddExchange, { + await wallet.client.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: args.exchangesUpdateCmd.url, - forceUpdate: args.exchangesUpdateCmd.force, + force: args.exchangesUpdateCmd.force, }); }); }); exchangesCli + .subcommand("exchangesShowCmd", "show", { + help: "Show exchange details", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.GetExchangeDetailedInfo, + { + exchangeBaseUrl: args.exchangesShowCmd.url, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +exchangesCli .subcommand("exchangesAddCmd", "add", { help: "Add an exchange by base URL.", }) @@ -464,19 +832,32 @@ exchangesCli }); exchangesCli + .subcommand("exchangesAddCmd", "delete", { + help: "Delete an exchange by base URL.", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .flag("purge", ["--purge"]) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: args.exchangesAddCmd.url, + purge: args.exchangesAddCmd.purge, + }); + }); + }); + +exchangesCli .subcommand("exchangesAcceptTosCmd", "accept-tos", { help: "Accept terms of service.", }) .requiredArgument("url", clk.STRING, { help: "Base URL of the exchange.", }) - .requiredArgument("etag", clk.STRING, { - help: "ToS version tag to accept", - }) .action(async (args) => { await withWallet(args, async (wallet) => { await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, { - etag: args.exchangesAcceptTosCmd.etag, exchangeBaseUrl: args.exchangesAcceptTosCmd.url, }); }); @@ -484,17 +865,26 @@ exchangesCli exchangesCli .subcommand("exchangesTosCmd", "tos", { - help: "Show terms of service.", + help: "Show/request terms of service.", }) .requiredArgument("url", clk.STRING, { help: "Base URL of the exchange.", }) + .maybeOption("contentTypes", ["--content-type"], clk.STRING) .action(async (args) => { + let acceptedFormat: string[] | undefined = undefined; + if (args.exchangesTosCmd.contentTypes) { + const split = args.exchangesTosCmd.contentTypes + .split(",") + .map((x) => x.trim()); + acceptedFormat = split; + } await withWallet(args, async (wallet) => { const tosResult = await wallet.client.call( WalletApiOperation.GetExchangeTos, { exchangeBaseUrl: args.exchangesTosCmd.url, + acceptedFormat, }, ); console.log(JSON.stringify(tosResult, undefined, 2)); @@ -505,96 +895,72 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", { help: "Subcommands for backups", }); -backupCli - .subcommand("setDeviceId", "set-device-id") - .requiredArgument("deviceId", clk.STRING, { - help: "new device ID", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.SetWalletDeviceId, { - walletDeviceId: args.setDeviceId.deviceId, - }); - }); - }); - -backupCli.subcommand("exportPlain", "export-plain").action(async (args) => { +backupCli.subcommand("exportDb", "export-db").action(async (args) => { await withWallet(args, async (wallet) => { - const backup = await wallet.client.call( - WalletApiOperation.ExportBackupPlain, - {}, - ); + const backup = await wallet.client.call(WalletApiOperation.ExportDb, {}); console.log(JSON.stringify(backup, undefined, 2)); }); }); -backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => { +backupCli.subcommand("storeBackup", "store").action(async (args) => { await withWallet(args, async (wallet) => { - const recoveryJson = await wallet.client.call( - WalletApiOperation.ExportBackupRecovery, + const resp = await wallet.client.call( + WalletApiOperation.CreateStoredBackup, {}, ); - console.log(JSON.stringify(recoveryJson, undefined, 2)); - }); -}); - -backupCli.subcommand("run", "run").action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); + console.log(JSON.stringify(resp, undefined, 2)); }); }); -backupCli.subcommand("status", "status").action(async (args) => { +backupCli.subcommand("storeBackup", "list-stored").action(async (args) => { await withWallet(args, async (wallet) => { - const status = await wallet.client.call( - WalletApiOperation.GetBackupInfo, + const resp = await wallet.client.call( + WalletApiOperation.ListStoredBackups, {}, ); - console.log(JSON.stringify(status, undefined, 2)); + console.log(JSON.stringify(resp, undefined, 2)); }); }); backupCli - .subcommand("recoveryLoad", "load-recovery") - .maybeOption("strategy", ["--strategy"], clk.STRING, { - help: - "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", - }) + .subcommand("storeBackup", "delete-stored") + .requiredArgument("name", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const data = JSON.parse(await read(process.stdin)); - let strategy: RecoveryMergeStrategy | undefined; - const stratStr = args.recoveryLoad.strategy; - if (stratStr) { - if (stratStr === "theirs") { - strategy = RecoveryMergeStrategy.Theirs; - } else if (stratStr === "ours") { - strategy = RecoveryMergeStrategy.Theirs; - } else { - throw Error("invalid recovery strategy"); - } - } - await wallet.client.call(WalletApiOperation.ImportBackupRecovery, { - recovery: data, - strategy, - }); + const resp = await wallet.client.call( + WalletApiOperation.DeleteStoredBackup, + { + name: args.storeBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); }); }); backupCli - .subcommand("addProvider", "add-provider") - .requiredArgument("url", clk.STRING) - .maybeArgument("name", clk.STRING) - .flag("activate", ["--activate"]) + .subcommand("recoverBackup", "recover-stored") + .requiredArgument("name", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: args.addProvider.url, - activate: args.addProvider.activate, - name: args.addProvider.name || args.addProvider.url, - }); + const resp = await wallet.client.call( + WalletApiOperation.RecoverStoredBackup, + { + name: args.recoverBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +backupCli.subcommand("importDb", "import-db").action(async (args) => { + await withWallet(args, async (wallet) => { + const dumpRaw = await read(process.stdin); + const dump = JSON.parse(dumpRaw); + await wallet.client.call(WalletApiOperation.ImportDb, { + dump, }); }); +}); const depositCli = walletCli.subcommand("depositArgs", "deposit", { help: "Subcommands for depositing money to payto:// accounts", @@ -602,7 +968,7 @@ const depositCli = walletCli.subcommand("depositArgs", "deposit", { depositCli .subcommand("createDepositArgs", "create") - .requiredArgument("amount", clk.STRING) + .requiredArgument("amount", clk.AMOUNT) .requiredArgument("targetPayto", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { @@ -614,19 +980,190 @@ depositCli }, ); console.log(`Created deposit ${resp.depositGroupId}`); - await wallet.ws.runPending(); }); }); -depositCli - .subcommand("trackDepositArgs", "track") - .requiredArgument("depositGroupId", clk.STRING) +const peerCli = walletCli.subcommand("peerArgs", "p2p", { + help: "Subcommands for peer-to-peer payments.", +}); + +peerCli + .subcommand("checkPayPush", "check-push-debit", { + help: "Check fees for starting a peer-push debit transaction.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to pay", + }) .action(async (args) => { await withWallet(args, async (wallet) => { const resp = await wallet.client.call( - WalletApiOperation.TrackDepositGroup, + WalletApiOperation.CheckPeerPushDebit, { - depositGroupId: args.trackDepositArgs.depositGroupId, + amount: args.checkPayPush.amount, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("checkPayPull", "check-pull-credit", { + help: "Check fees for a starting peer-pull credit transaction.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to request", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.CheckPeerPullCredit, + { + amount: args.checkPayPull.amount, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("prepareIncomingPayPull", "prepare-pull-debit") + .requiredArgument("talerUri", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.PreparePeerPullDebit, + { + talerUri: args.prepareIncomingPayPull.talerUri, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("confirmIncomingPayPull", "confirm-pull-debit") + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ConfirmPeerPullDebit, + { + transactionId: args.confirmIncomingPayPull + .transactionId as TransactionIdStr, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("confirmIncomingPayPush", "confirm-push-credit") + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ConfirmPeerPushCredit, + { + transactionId: args.confirmIncomingPayPush.transactionId, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("initiatePayPull", "initiate-pull-credit", { + help: "Initiate a peer-pull payment.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to request", + }) + .maybeOption("summary", ["--summary"], clk.STRING, { + help: "Summary to use in the contract terms.", + }) + .maybeOption("purseExpiration", ["--purse-expiration"], clk.STRING) + .maybeOption("exchangeBaseUrl", ["--exchange"], clk.STRING) + .action(async (args) => { + let purseExpiration: AbsoluteTime; + + if (args.initiatePayPull.purseExpiration) { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromPrettyString(args.initiatePayPull.purseExpiration), + ); + } else { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ); + } + + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.InitiatePeerPullCredit, + { + exchangeBaseUrl: args.initiatePayPull.exchangeBaseUrl, + partialContractTerms: { + amount: args.initiatePayPull.amount, + summary: args.initiatePayPull.summary ?? "Invoice", + purse_expiration: AbsoluteTime.toProtocolTimestamp(purseExpiration), + }, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("preparePushCredit", "prepare-push-credit") + .requiredArgument("talerUri", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.PreparePeerPushCredit, + { + talerUri: args.preparePushCredit.talerUri, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("payPush", "initiate-push-debit", { + help: "Initiate a peer-push payment.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to pay", + }) + .maybeOption("summary", ["--summary"], clk.STRING, { + help: "Summary to use in the contract terms.", + }) + .maybeOption("purseExpiration", ["--purse-expiration"], clk.STRING) + .action(async (args) => { + let purseExpiration: AbsoluteTime; + + if (args.payPush.purseExpiration) { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromPrettyString(args.payPush.purseExpiration), + ); + } else { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ); + } + + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.InitiatePeerPushDebit, + { + partialContractTerms: { + amount: args.payPush.amount, + summary: args.payPush.summary ?? "Payment", + purse_expiration: AbsoluteTime.toProtocolTimestamp(purseExpiration), + }, }, ); console.log(JSON.stringify(resp, undefined, 2)); @@ -634,148 +1171,323 @@ depositCli }); const advancedCli = walletCli.subcommand("advancedArgs", "advanced", { - help: - "Subcommands for advanced operations (only use if you know what you're doing!).", + help: "Subcommands for advanced operations (only use if you know what you're doing!).", }); advancedCli - .subcommand("bench1", "bench1", { - help: "Run the 'bench1' benchmark", + .subcommand("genReserve", "gen-reserve", { + help: "Generate a reserve key pair (not stored in the DB).", }) - .requiredOption("configJson", ["--config-json"], clk.STRING) .action(async (args) => { - let config: any; - try { - config = JSON.parse(args.bench1.configJson); - } catch (e) { - console.log("Could not parse config JSON"); - } - await runBench1(config); + const pair = await nativeCrypto.createEddsaKeypair({}); + console.log( + j2s({ + reservePub: pair.pub, + reservePriv: pair.priv, + }), + ); }); advancedCli - .subcommand("env1", "env1", { - help: "Run a test environment for bench1", + .subcommand("tasks", "tasks", { + help: "Show active wallet-core tasks.", }) .action(async (args) => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-")); - const testState = new GlobalTestState({ - testDir, + await withWallet(args, async (wallet) => { + const tasks = await wallet.client.call( + WalletApiOperation.GetActiveTasks, + {}, + ); + console.log(j2s(tasks)); }); - await runTestWithState(testState, runEnv1, "env1", true); }); advancedCli - .subcommand("withdrawFakebank", "withdraw-fakebank", { - help: "Withdraw via a fakebank.", + .subcommand("sampleTransactions", "sample-transactions", { + help: "Print sample wallet-core transactions", }) - .requiredOption("exchange", ["--exchange"], clk.STRING, { - help: "Base URL of the exchange to use", + .action(async (args) => { + console.log(JSON.stringify(sampleWalletCoreTransactions, undefined, 2)); + }); + +advancedCli + .subcommand("serve", "serve", { + help: "Serve the wallet API via a unix domain socket.", }) - .requiredOption("amount", ["--amount"], clk.STRING, { - help: "Amount to withdraw (before fees).", + .requiredOption("unixPath", ["--unix-path"], clk.STRING, { + default: defaultWalletCoreSocket, }) - .requiredOption("bank", ["--bank"], clk.STRING, { - help: "Base URL of the Taler fakebank service.", + .flag("noInit", ["--no-init"], { + help: "Do not initialize the wallet. The client must send the initWallet message.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.WithdrawFakebank, { - amount: args.withdrawFakebank.amount, - bank: args.withdrawFakebank.bank, - exchange: args.withdrawFakebank.exchange, + const socketPath = args.serve.unixPath; + logger.info(`serving at ${socketPath}`); + let cleanupCalled = false; + + const cleanupSocket = (signal: string, code: number) => { + if (cleanupCalled) { + return; + } + cleanupCalled = true; + try { + logger.info("cleaning up socket"); + fs.unlinkSync(socketPath); + } catch (e) { + logger.warn(`unable to clean up socket: ${e}`); + } + process.exit(128 + code); + }; + process.on("SIGTERM", cleanupSocket); + process.on("SIGINT", cleanupSocket); + + const onNotif = (notif: WalletNotification) => { + writeObservabilityLog(notif); + }; + const wh = await createLocalWallet(args, onNotif, args.serve.noInit); + const w = wh.wallet; + let nextClientId = 1; + const notifyHandlers = new Map<number, (n: WalletNotification) => void>(); + w.addNotificationListener((n) => { + notifyHandlers.forEach((v, k) => { + v(n); }); }); + await runRpcServer({ + socketFilename: args.serve.unixPath, + onConnect(client) { + logger.info("connected"); + const clientId = nextClientId++; + notifyHandlers.set(clientId, (n: WalletNotification) => { + client.sendResponse({ + type: "notification", + payload: n as unknown as JsonMessage, + }); + }); + return { + onDisconnect() { + notifyHandlers.delete(clientId); + logger.info("disconnected"); + }, + onMessage(msg) { + logger.info(`message: ${j2s(msg)}`); + const op = (msg as any).operation; + const id = (msg as any).id; + const payload = (msg as any).args; + w.handleCoreApiRequest(op, id, payload) + .then((resp) => { + logger.info("sending response"); + client.sendResponse(resp as unknown as JsonMessage); + }) + .catch((e) => { + logger.error(`unexpected error: ${e}`); + }); + }, + }; + }, + }); }); advancedCli - .subcommand("manualWithdrawalDetails", "manual-withdrawal-details", { - help: "Query withdrawal fees.", + .subcommand("init", "init", { + help: "Initialize the wallet (with DB) and exit.", + }) + .action(async (args) => { + await withWallet(args, async () => {}); + }); + +advancedCli + .subcommand("runPendingOpt", "run-pending", { + help: "Run pending operations.", }) - .requiredArgument("exchange", clk.STRING) - .requiredArgument("amount", clk.STRING) + .action(async (args) => { + logger.error( + "Subcommand run-pending not supported anymore. Please use run-until-done or the client/server wallet.", + ); + }); + +advancedCli + .subcommand("pending", "pending", { help: "Show pending operations." }) .action(async (args) => { await withWallet(args, async (wallet) => { - const details = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForAmount, - { - amount: args.manualWithdrawalDetails.amount, - exchangeBaseUrl: args.manualWithdrawalDetails.exchange, - }, + const pending = await wallet.client.call( + WalletApiOperation.GetPendingOperations, + {}, ); - console.log(JSON.stringify(details, undefined, 2)); + console.log(JSON.stringify(pending, undefined, 2)); }); }); advancedCli - .subcommand("decode", "decode", { - help: "Decode base32-crockford.", + .subcommand("benchInternal", "bench-internal", { + help: "Run the 'bench-internal' benchmark", }) - .action((args) => { - const enc = fs.readFileSync(0, "utf8"); - fs.writeFileSync(1, decodeCrock(enc.trim())); + .action(async (args) => { + const myHttpLib = createPlatformHttpLib(); + const res = await createNativeWalletHost2({ + // No persistent DB storage. + persistentStoragePath: undefined, + httpLib: myHttpLib, + }); + const wallet = res.wallet; + await wallet.client.call(WalletApiOperation.InitWallet, {}); + await wallet.client.call(WalletApiOperation.RunIntegrationTest, { + amountToSpend: "TESTKUDOS:1" as AmountString, + amountToWithdraw: "TESTKUDOS:3" as AmountString, + corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/", + exchangeBaseUrl: "http://localhost:8081/", + merchantBaseUrl: "http://localhost:8083/", + }); + await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {}); + await wallet.client.call(WalletApiOperation.Shutdown, {}); }); advancedCli - .subcommand("withdrawManually", "withdraw-manually", { - help: "Withdraw manually from an exchange.", + .subcommand("genSegwit", "gen-segwit") + .requiredArgument("paytoUri", clk.STRING) + .requiredArgument("reservePub", clk.STRING) + .action(async (args) => { + const p = parsePaytoUri(args.genSegwit.paytoUri); + console.log(p); + }); + +const currenciesCli = walletCli.subcommand("currencies", "currencies", { + help: "Manage currencies.", +}); + +currenciesCli + .subcommand("listGlobalAuditors", "list-global-auditors", { + help: "List global-currency auditors.", }) - .requiredOption("exchange", ["--exchange"], clk.STRING, { - help: "Base URL of the exchange.", + .action(async (args) => { + await withWallet(args, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.ListGlobalCurrencyAuditors, + {}, + ); + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); + +currenciesCli + .subcommand("listGlobalExchanges", "list-global-exchanges", { + help: "List global-currency exchanges.", }) - .requiredOption("amount", ["--amount"], clk.STRING, { - help: "Amount to withdraw", + .action(async (args) => { + await withWallet(args, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.ListGlobalCurrencyExchanges, + {}, + ); + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); + +currenciesCli + .subcommand("addGlobalExchange", "add-global-exchange", { + help: "Add a global-currency exchange.", }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING) + .requiredOption("exchangePub", ["--pub"], clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const exchangeBaseUrl = args.withdrawManually.exchange; - const amount = args.withdrawManually.amount; - const d = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForAmount, + const currencies = await wallet.client.call( + WalletApiOperation.AddGlobalCurrencyExchange, { - amount: args.withdrawManually.amount, - exchangeBaseUrl: exchangeBaseUrl, + currency: args.addGlobalExchange.currency, + exchangeBaseUrl: args.addGlobalExchange.exchangeBaseUrl, + exchangeMasterPub: args.addGlobalExchange.exchangePub, }, ); - const acct = d.paytoUris[0]; - if (!acct) { - console.log("exchange has no accounts"); - return; - } - const resp = await wallet.client.call( - WalletApiOperation.AcceptManualWithdrawal, + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); + +currenciesCli + .subcommand("removeGlobalExchange", "remove-global-exchange", { + help: "Remove a global-currency exchange.", + }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING) + .requiredOption("exchangePub", ["--pub"], clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.RemoveGlobalCurrencyExchange, { - amount, - exchangeBaseUrl, + currency: args.removeGlobalExchange.currency, + exchangeBaseUrl: args.removeGlobalExchange.exchangeBaseUrl, + exchangeMasterPub: args.removeGlobalExchange.exchangePub, }, ); - const reservePub = resp.reservePub; - const completePaytoUri = addPaytoQueryParams(acct, { - amount: args.withdrawManually.amount, - message: `Taler top-up ${reservePub}`, - }); - console.log("Created reserve", reservePub); - console.log("Payto URI", completePaytoUri); + console.log(JSON.stringify(currencies, undefined, 2)); }); }); -const currenciesCli = walletCli.subcommand("currencies", "currencies", { - help: "Manage currencies.", -}); +currenciesCli + .subcommand("addGlobalAuditor", "add-global-auditor", { + help: "Add a global-currency auditor.", + }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("auditorBaseUrl", ["--url"], clk.STRING) + .requiredOption("auditorPub", ["--pub"], clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.AddGlobalCurrencyAuditor, + { + currency: args.addGlobalAuditor.currency, + auditorBaseUrl: args.addGlobalAuditor.auditorBaseUrl, + auditorPub: args.addGlobalAuditor.auditorPub, + }, + ); + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); currenciesCli - .subcommand("show", "show", { help: "Show currencies." }) + .subcommand("removeGlobalAuditor", "remove-global-auditor", { + help: "Remove a global-currency auditor.", + }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("auditorBaseUrl", ["--url"], clk.STRING) + .requiredOption("auditorPub", ["--pub"], clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { const currencies = await wallet.client.call( - WalletApiOperation.ListCurrencies, - {}, + WalletApiOperation.RemoveGlobalCurrencyAuditor, + { + currency: args.removeGlobalAuditor.currency, + auditorBaseUrl: args.removeGlobalAuditor.auditorBaseUrl, + auditorPub: args.removeGlobalAuditor.auditorPub, + }, ); console.log(JSON.stringify(currencies, undefined, 2)); }); }); advancedCli + .subcommand("clearDatabase", "clear-database", { + help: "Clear the database, irrevocable deleting all data in the wallet.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.ClearDb, {}); + }); + }); + +advancedCli + .subcommand("recycle", "recycle", { + help: "Export, clear and re-import the database via the backup mechanism.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.Recycle, {}); + }); + }); + +advancedCli .subcommand("payPrepare", "pay-prepare", { help: "Claim an order but don't pay yet.", }) @@ -809,6 +1521,19 @@ advancedCli }); advancedCli + .subcommand("queryRefund", "query-refund", { + help: "Query refunds for a payment transaction.", + }) + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.StartRefundQuery, { + transactionId: args.queryRefund.transactionId as TransactionIdStr, + }); + }); + }); + +advancedCli .subcommand("payConfirm", "pay-confirm", { help: "Confirm payment proposed by a merchant.", }) @@ -831,7 +1556,11 @@ advancedCli .action(async (args) => { await withWallet(args, async (wallet) => { await wallet.client.call(WalletApiOperation.ForceRefresh, { - coinPubList: [args.refresh.coinPub], + refreshCoinSpecs: [ + { + coinPub: args.refresh.coinPub, + }, + ], }); }); }); @@ -866,7 +1595,7 @@ advancedCli ); } catch (e: any) { console.log("could not parse coin list:", e.message); - process.exit(1); + processExit(1); } for (const c of coinPubList) { await wallet.client.call(WalletApiOperation.SetCoinSuspended, { @@ -891,7 +1620,7 @@ advancedCli ); } catch (e: any) { console.log("could not parse coin list:", e.message); - process.exit(1); + processExit(1); } for (const c of coinPubList) { await wallet.client.call(WalletApiOperation.SetCoinSuspended, { @@ -913,210 +1642,164 @@ advancedCli console.log(`coin ${coin.coin_pub}`); console.log(` exchange ${coin.exchange_base_url}`); console.log(` denomPubHash ${coin.denom_pub_hash}`); - console.log( - ` remaining amount ${Amounts.stringify(coin.remaining_value)}`, - ); + console.log(` status ${coin.coin_status}`); } }); }); -const deploymentCli = walletCli.subcommand("deploymentArgs", "deployment", { - help: "Subcommands for handling GNU Taler deployments.", -}); - -deploymentCli - .subcommand("lintExchange", "lint-exchange", { - help: "Run checks on the exchange deployment.", - }) - .flag("cont", ["--continue"], { - help: "Continue after errors if possible", - }) - .flag("debug", ["--debug"], { - help: "Output extra debug info", - }) - .action(async (args) => { - await lintExchangeDeployment( - args.lintExchange.debug, - args.lintExchange.cont, - ); - }); - -deploymentCli - .subcommand("coincfg", "gen-coin-config", { - help: "Generate a coin/denomination configuration for the exchange.", - }) - .requiredOption("minAmount", ["--min-amount"], clk.STRING, { - help: "Smallest denomination", - }) - .requiredOption("maxAmount", ["--max-amount"], clk.STRING, { - help: "Largest denomination", - }) - .action(async (args) => { - let out = ""; - - const stamp = Math.floor(new Date().getTime() / 1000); - - const min = Amounts.parseOrThrow(args.coincfg.minAmount); - const max = Amounts.parseOrThrow(args.coincfg.maxAmount); - if (min.currency != max.currency) { - console.error("currency mismatch"); - process.exit(1); - } - const currency = min.currency; - let x = min; - let n = 1; - - out += "# Coin configuration for the exchange.\n"; - out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n'; - out += "\n"; - - while (Amounts.cmp(x, max) < 0) { - out += `[COIN-${currency}-n${n}-t${stamp}]\n`; - out += `VALUE = ${Amounts.stringify(x)}\n`; - out += `DURATION_WITHDRAW = 7 days\n`; - out += `DURATION_SPEND = 2 years\n`; - out += `DURATION_LEGAL = 6 years\n`; - out += `FEE_WITHDRAW = ${currency}:0\n`; - out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`; - out += `FEE_REFRESH = ${currency}:0\n`; - out += `FEE_REFUND = ${currency}:0\n`; - out += `RSA_KEYSIZE = 2048\n`; - out += "\n"; - x = Amounts.add(x, x).amount; - n++; - } - - console.log(out); - }); - -const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", { - help: "Subcommands the Taler configuration.", -}); - -deploymentConfigCli - .subcommand("show", "show") - .flag("diagnostics", ["-d", "--diagnostics"]) - .maybeArgument("cfgfile", clk.STRING, {}) - .action(async (args) => { - const cfg = Configuration.load(args.show.cfgfile); - console.log( - cfg.stringify({ - diagnostics: args.show.diagnostics, - }), - ); - }); - const testCli = walletCli.subcommand("testingArgs", "testing", { help: "Subcommands for testing.", }); testCli - .subcommand("listIntegrationtests", "list-integrationtests") + .subcommand("withdrawTestkudos", "withdraw-testkudos") .action(async (args) => { - for (const t of getTestInfo()) { - let s = t.name; - if (t.suites.length > 0) { - s += ` (suites: ${t.suites.join(",")})`; - } - if (t.excludeByDefault) { - s += ` [excluded by default]`; - } - console.log(s); - } + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.WithdrawTestkudos, {}); + }); }); -testCli - .subcommand("runIntegrationtests", "run-integrationtests") - .maybeArgument("pattern", clk.STRING, { - help: "Glob pattern to select which tests to run", - }) - .maybeOption("suites", ["--suites"], clk.STRING, { - help: "Only run selected suites (comma-separated list)", - }) - .flag("dryRun", ["--dry"], { - help: "Only print tests that will be selected to run.", - }) - .flag("quiet", ["--quiet"], { - help: "Produce less output.", - }) - .action(async (args) => { - await runTests({ - includePattern: args.runIntegrationtests.pattern, - suiteSpec: args.runIntegrationtests.suites, - dryRun: args.runIntegrationtests.dryRun, - verbosity: args.runIntegrationtests.quiet ? 0 : 1, +testCli.subcommand("withdrawKudos", "withdraw-kudos").action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.WithdrawTestBalance, { + amount: "KUDOS:50" as AmountString, + corebankApiBaseUrl: "https://bank.demo.taler.net/", + exchangeBaseUrl: "https://exchange.demo.taler.net/", }); }); +}); -async function read(stream: NodeJS.ReadStream) { - const chunks = []; - for await (const chunk of stream) chunks.push(chunk); - return Buffer.concat(chunks).toString("utf8"); -} +class PerfTimer { + tStarted: bigint | undefined; + tSum = BigInt(0); + tSumSq = BigInt(0); -testCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => { - const data = await read(process.stdin); + start() { + this.tStarted = process.hrtime.bigint(); + } - const lines = data.match(/[^\r\n]+/g); + stop() { + const now = process.hrtime.bigint(); + const s = this.tStarted; + if (s == null) { + throw Error(); + } + this.tSum = this.tSum + (now - s); + this.tSumSq = this.tSumSq + (now - s) * (now - s); + } - if (!lines) { - throw Error("can't split lines"); + mean(nRuns: number): number { + return Number(this.tSum / BigInt(nRuns)); } - const vals: Record<string, string> = {}; + stdev(nRuns: number) { + const m = this.tSum / BigInt(nRuns); + const x = this.tSumSq / BigInt(nRuns) - m * m; + return Math.floor(Math.sqrt(Number(x))); + } +} - let inBlindSigningSection = false; +testCli + .subcommand("benchmarkAgeRestrictions", "benchmark-age-restrictions") + .requiredOption("reps", ["--reps"], clk.INT, { + default: 100, + help: "repetitions (default: 100)", + }) + .action(async (args) => { + const numReps = args.benchmarkAgeRestrictions.reps ?? 100; + let tCommit = new PerfTimer(); + let tAttest = new PerfTimer(); + let tVerify = new PerfTimer(); + let tDerive = new PerfTimer(); + let tCompare = new PerfTimer(); + + console.log("starting benchmark"); + + for (let i = 0; i < numReps; i++) { + console.log(`doing iteration ${i}`); + tCommit.start(); + const commitProof = await AgeRestriction.restrictionCommit( + 0b1000001010101010101001, + 21, + ); + tCommit.stop(); - for (const line of lines) { - if (line === "blind signing:") { - inBlindSigningSection = true; - continue; - } - if (line[0] !== " ") { - inBlindSigningSection = false; - continue; - } - if (inBlindSigningSection) { - const m = line.match(/ (\w+) (\w+)/); - if (!m) { - console.log("bad format"); - process.exit(2); + tAttest.start(); + const attest = AgeRestriction.commitmentAttest(commitProof, 18); + tAttest.stop(); + + tVerify.start(); + const attestRes = AgeRestriction.commitmentVerify( + commitProof.commitment, + encodeCrock(attest), + 18, + ); + tVerify.stop(); + if (!attestRes) { + throw Error(); } - vals[m[1]] = m[2]; - } - } - console.log(vals); + const salt = getRandomBytes(32); + tDerive.start(); + const deriv = await AgeRestriction.commitmentDerive(commitProof, salt); + tDerive.stop(); - const req = (k: string) => { - if (!vals[k]) { - throw Error(`no value for ${k}`); + tCompare.start(); + const res2 = await AgeRestriction.commitCompare( + deriv.commitment, + commitProof.commitment, + salt, + ); + tCompare.stop(); + if (!res2) { + throw Error(); + } } - return decodeCrock(vals[k]); - }; - - const myBm = rsaBlind( - req("message_hash"), - req("blinding_key_secret"), - req("rsa_public_key"), - ); - deepStrictEqual(req("blinded_message"), myBm); + console.log( + `edx25519-commit (ns): ${tCommit.mean(numReps)} (stdev ${tCommit.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-attest (ns): ${tAttest.mean(numReps)} (stdev ${tAttest.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-verify (ns): ${tVerify.mean(numReps)} (stdev ${tVerify.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-derive (ns): ${tDerive.mean(numReps)} (stdev ${tDerive.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-compare (ns): ${tCompare.mean(numReps)} (stdev ${tCompare.stdev( + numReps, + )})`, + ); + }); - console.log("check passed!"); +testCli.subcommand("logtest", "logtest").action(async (args) => { + logger.trace("This is a trace message."); + logger.info("This is an info message."); + logger.warn("This is an warning message."); + logger.error("This is an error message."); }); -testCli.subcommand("cryptoworker", "cryptoworker").action(async (args) => { - const workerFactory = new NodeThreadCryptoWorkerFactory(); - const cryptoApi = new CryptoApi(workerFactory); - const res = await cryptoApi.hashString("foo"); - console.log(res); -}); +async function read(stream: NodeJS.ReadStream) { + const chunks = []; + for await (const chunk of stream) chunks.push(chunk); + return Buffer.concat(chunks).toString("utf8"); +} export function main() { - if (process.env["TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE"]) { - logger.warn("Allowing withdrawal of late denominations for debugging"); - walletCoreDebugFlags.denomselAllowLate = true; + const maybeFilename = getenv("TALER_WALLET_DEBUG_OBSERVE"); + if (!!maybeFilename) { + observabilityEventFile = maybeFilename; } walletCli.run(); } |