taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5651a7a0219817936487a473959570e86a9c2d42
parent 2fb8ecb717a230d9e97696336f3196707f556f1b
Author: Florian Dold <dold@taler.net>
Date:   Mon, 10 Aug 2026 01:09:17 +0200

wallet-cli: unify withdrawal commands

Diffstat:
Mpackages/taler-wallet-cli/src/index.ts | 244++++++++++++++++++++++++++++++++++++-------------------------------------------
1 file changed, 112 insertions(+), 132 deletions(-)

diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts @@ -2240,72 +2240,130 @@ walletCli }); }); -const withdrawCli = walletCli.subcommand("withdraw", "withdraw", { - help: "Withdraw with a taler://withdraw/ URI", -}); - -withdrawCli - .subcommand("withdrawCheckUri", "check-uri", { mark: "legacy" }) - .requiredArgument("uri", clk.STRING) +const withdrawCli = walletCli + .subcommand("withdraw", "withdraw", { + help: "Withdraw from a bank-integrated URI or directly from an exchange.", + }) + .maybeOption("uri", ["--uri"], clk.STRING, { + help: "Bank-integrated taler://withdraw/ URI.", + }) + .maybeOption("exchange", ["--exchange"], clk.STRING, { + help: "Base URL of the exchange (required without a URI).", + }) + .maybeOption("amount", ["--amount"], clk.AMOUNT, { + help: "Amount to withdraw (required without a URI).", + }) + .flag("check", ["--check"], { + help: "Show withdrawal details and fees without creating a transaction.", + }) + .maybeOption("forcedReservePriv", ["--forced-reserve-priv"], clk.STRING) .maybeOption("restrictAge", ["--restrict-age"], clk.INT) - .action(async (args) => { - const uri = args.withdrawCheckUri.uri; - const restrictAge = args.withdrawCheckUri.restrictAge; - logger.info(`age restriction requested (${restrictAge})`); - await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { - const withdrawInfo = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForUri, - { - talerWithdrawUri: uri, - restrictAge, - }, - ); - console.log("withdrawInfo", withdrawInfo); - }); + .flag("wait", ["--wait"], { + help: "Wait until the transaction is in a final state.", + }) + .maybeOption("timeout", ["--timeout"], clk.STRING, { + help: "Give up waiting after this duration (e.g. '30s', '5m').", }); -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; - logger.info(`age restriction requested (${restrictAge})`); - await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { - const withdrawInfo = await wallet.client.call( +withdrawCli.action(async (args) => { + await runCliAction(() => + withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const { uri, exchange, amount, restrictAge } = args.withdraw; + if (uri != null) { + if (args.withdraw.forcedReservePriv != null) { + throw new CliUsageError( + "--forced-reserve-priv is only available for direct exchange withdrawals", + "omit the URI to use a direct exchange withdrawal", + ); + } + const details = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: uri, + restrictAge, + }, + ); + if (args.withdraw.check) { + console.log(j2s(details)); + return; + } + const exchangeBaseUrl = exchange ?? details.defaultExchangeBaseUrl; + if (exchangeBaseUrl == null) { + throw new CliUsageError( + "no exchange was specified or suggested by the withdrawal URI", + "pass --exchange EXCHANGE_URL", + ); + } + const res = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl, + talerWithdrawUri: uri, + amount, + restrictAge, + }, + ); + console.log(`transaction ${res.transactionId}`); + return await waitForCreatedTx(wallet, res.transactionId, args.withdraw); + } + + if (exchange == null || amount == null) { + throw new CliUsageError( + "direct withdrawals require both --exchange and --amount", + "pass a TALER_WITHDRAW_URI for a bank-integrated withdrawal", + ); + } + const details = await wallet.client.call( WalletApiOperation.GetWithdrawalDetailsForAmount, { - amount: args.withdrawCheckAmount.amount, - exchangeBaseUrl: args.withdrawCheckAmount.exchange, + amount, + exchangeBaseUrl: exchange, restrictAge, }, ); - console.log("withdrawInfo", withdrawInfo); - }); - }); - -withdrawCli - .subcommand("withdrawAcceptUri", "accept-uri", { mark: "legacy" }) - .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, { lazyTaskLoop: true }, async (wallet) => { - const res = await wallet.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, + if (args.withdraw.check) { + console.log(j2s(details)); + return; + } + if (details.withdrawalAccountsList.length === 0) { + console.log("exchange has no accounts"); + return EXIT_API_ERROR; + } + const resp = await wallet.client.call( + WalletApiOperation.AcceptManualWithdrawal, { - exchangeBaseUrl: args.withdrawAcceptUri.exchange, - talerWithdrawUri: uri, + amount, + exchangeBaseUrl: exchange, restrictAge, + forceReservePriv: args.withdraw.forcedReservePriv, }, ); - console.log(j2s(res)); - }); - }); + await wallet.client.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: resp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.ExchangeWaitReserve, + }, + }); + const txDet = await wallet.client.call( + WalletApiOperation.GetTransactionById, + { + transactionId: resp.transactionId, + }, + ); + if (txDet.type !== TransactionType.Withdrawal) { + throw Error("assertion failed"); + } + if (txDet.withdrawalDetails.type !== WithdrawalType.ManualTransfer) { + throw Error("assertion failed"); + } + console.log(`transaction ${resp.transactionId}`); + console.log("transfer accounts:"); + console.log(j2s(txDet.withdrawalDetails.exchangeCreditAccountDetails)); + return await waitForCreatedTx(wallet, resp.transactionId, args.withdraw); + }), + ); +}); async function cliHandleTos( wallet: WalletContext, @@ -2598,84 +2656,6 @@ walletCli }); }); -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) - .flag("wait", ["--wait"], { - help: "Wait until the transaction is in a final state.", - }) - .maybeOption("timeout", ["--timeout"], clk.STRING, { - help: "Give up waiting after this duration (e.g. '30s', '5m').", - }) - .action(async (args) => { - await runCliAction(() => - withWallet(args, { lazyTaskLoop: true }, 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 EXIT_API_ERROR; - } - const resp = await wallet.client.call( - WalletApiOperation.AcceptManualWithdrawal, - { - amount, - exchangeBaseUrl, - restrictAge: args.withdrawManually.restrictAge, - forceReservePriv: args.withdrawManually.forcedReservePriv, - }, - ); - await wallet.client.call( - WalletApiOperation.TestingWaitTransactionState, - { - transactionId: resp.transactionId, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.ExchangeWaitReserve, - }, - }, - ); - const txDet = await wallet.client.call( - WalletApiOperation.GetTransactionById, - { - transactionId: resp.transactionId, - }, - ); - if (txDet.type !== TransactionType.Withdrawal) { - throw Error("assertion failed"); - } - if (txDet.withdrawalDetails.type !== WithdrawalType.ManualTransfer) { - throw Error("assertion failed"); - } - console.log(`transaction ${resp.transactionId}`); - console.log("transfer accounts:"); - console.log(j2s(txDet.withdrawalDetails.exchangeCreditAccountDetails)); - return await waitForCreatedTx( - wallet, - resp.transactionId, - args.withdrawManually, - ); - }), - ); - }); - const exchangesCli = walletCli .subcommand("exchangesCmd", "exchanges", { help: [