summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-cli/src/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-cli/src/index.ts')
-rw-r--r--packages/taler-wallet-cli/src/index.ts1685
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();
}