taler-typescript-core

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

commit acd9b6eb06632aa31c243e8cfd85abed080bb4ca
parent 3ee22ae6d425f5c937d9213f3d25d2510150446a
Author: Florian Dold <florian@dold.me>
Date:   Tue, 12 May 2026 18:28:24 +0200

wallet-cli: be more agent-compatible in handle-uri

Diffstat:
Mpackages/taler-wallet-cli/src/index.ts | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mpackages/taler-wallet-core/src/common.ts | 15+++++++++++++++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 11+++++++----
Mpackages/taler-wallet-core/src/refresh.ts | 13+++++--------
Mpackages/taler-wallet-core/src/shepherd.ts | 3+++
5 files changed, 94 insertions(+), 32 deletions(-)

diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts @@ -90,6 +90,7 @@ let observabilityEventFile: string | undefined = undefined; const EXIT_EXCEPTION = 4; const EXIT_API_ERROR = 5; +const EXIT_INPUT_REQUIRED = 6; setUnhandledRejectionHandler((error: any) => { logger.error("unhandledRejection", error.message); @@ -100,26 +101,36 @@ setUnhandledRejectionHandler((error: any) => { const defaultWalletDbPath = pathHomedir() + "/" + ".talerwalletdb.sqlite3"; const defaultWalletCoreSocket = pathHomedir() + "/" + ".wallet-core.sock"; +interface PayOptions { + alwaysYes?: boolean; + nonInteractive?: boolean; + choiceIndex?: number; + noWait?: boolean; + verbose?: number; +} + async function doPayTemplate( wallet: WalletCoreApiClient, payUrl: string, - options: { alwaysYes: boolean } = { alwaysYes: true }, + options: PayOptions = {}, ): Promise<void> { const result = await wallet.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri: payUrl, }); + console.log(`Instantiated payment template as ${result.transactionId}`); if (result.status === PreparePayResultType.InsufficientBalance) { console.log("contract", result.contractTerms); console.error("insufficient balance"); processExit(1); return; } - return doPay(wallet, result.talerUri, options) + return doPay(wallet, result.talerUri, options); } + async function doPay( wallet: WalletCoreApiClient, payUrl: string, - options: { alwaysYes: boolean } = { alwaysYes: true }, + options: PayOptions = {}, ): Promise<void> { const result = await wallet.call(WalletApiOperation.PreparePayForUri, { talerPayUri: payUrl, @@ -133,44 +144,54 @@ async function doPay( let choiceIndex: number | undefined; if (result.status === PreparePayResultType.AlreadyConfirmed) { if (result.paid) { - console.log("already paid!"); + console.log(`Already paid (transactionId ${result.transactionId})`); } else { - console.log("payment already in progress"); + console.log( + `Payment already in progress (transactionId ${result.transactionId})`, + ); } processExit(0); return; } else if (result.status === PreparePayResultType.ChoiceSelection) { - console.log(`choices:`); const choices = await wallet.call(WalletApiOperation.GetChoicesForPayment, { transactionId: result.transactionId, }); - console.log(j2s(choices)); - choiceIndex = await askChoice(choices.choices.length); + if (options.choiceIndex != null) { + choiceIndex = options.choiceIndex; + } else if (choices.choices.length > 1) { + if (options.nonInteractive) { + console.log(`choices:`); + console.log(`Please choose an option with --choice-index.`); + processExit(EXIT_INPUT_REQUIRED); + } else { + console.log(`choices:`); + choiceIndex = await askChoice(choices.choices.length); + } + } else { + choiceIndex = 0; + console.log("contract:", result.contractTerms); + } const myChoice = choices.choices[choiceIndex]; if (myChoice.status !== ChoiceSelectionDetailType.PaymentPossible) { console.log("insufficient balance for choice"); processExit(1); } - console.log("paying ..."); - console.log("contract", result.contractTerms); - console.log("raw amount:", myChoice.amountRaw); - console.log("effective amount:", myChoice.amountEffective); } else if (result.status === "payment-possible") { - console.log("paying ..."); - console.log("contract", result.contractTerms); - console.log("raw amount:", result.amountRaw); - console.log("effective amount:", result.amountEffective); + console.log("contract:", result.contractTerms); } else { throw Error("not reached"); } - let pay: boolean; + let doPay: boolean; if (options.alwaysYes) { - pay = true; + doPay = true; + } else if (options.nonInteractive) { + console.log(`Please confirm payment by passing '--yes' to handle-uri`); + processExit(EXIT_INPUT_REQUIRED); } else { - pay = await askYesNo(); + doPay = await askYesNo(); } - if (pay) { + if (doPay) { await wallet.call(WalletApiOperation.ConfirmPay, { transactionId: result.transactionId, choiceIndex: choiceIndex, @@ -179,6 +200,20 @@ async function doPay( } else { console.log("not paying"); } + if (options.noWait) { + return; + } + console.log(`Waiting for transaction '${result.transactionId}' to finish`); + await wallet.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: result.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); + const tx = await wallet.call(WalletApiOperation.GetTransactionById, { + transactionId: result.transactionId, + }); + console.log(`Finished with status '${tx.txState.major}'.`); } async function askChoice(n: number): Promise<number> { @@ -899,7 +934,10 @@ walletCli .maybeOption("withdrawalExchange", ["--withdrawal-exchange"], clk.STRING, { help: "Exchange to use for withdrawal operations.", }) + .flag("noWait", ["--no-wait"]) + .maybeOption("choiceIndex", ["--choice-index"], clk.INT) .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .flag("nonInteractive", ["--non-interactive"]) .flag("autoYes", ["-y", "--yes"]) .action(async (args) => { await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { @@ -917,11 +955,17 @@ walletCli case TalerUriAction.PayTemplate: await doPayTemplate(wallet.client, uri, { alwaysYes: args.handleUri.autoYes, + choiceIndex: args.handleUri.choiceIndex, + nonInteractive: args.handleUri.nonInteractive, + noWait: args.handleUri.noWait, }); break; case TalerUriAction.Pay: await doPay(wallet.client, uri, { alwaysYes: args.handleUri.autoYes, + choiceIndex: args.handleUri.choiceIndex, + nonInteractive: args.handleUri.nonInteractive, + noWait: args.handleUri.noWait, }); break; case TalerUriAction.Refund: diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -408,10 +408,12 @@ export enum TaskRunResultType { LongpollReturnedPending = "longpoll-returned-pending", ScheduleLater = "schedule-later", NetworkRequired = "network-required", + Cancelled = "cancelled", } export type TaskRunResult = | TaskRunFinishedResult + | TaskRunCancelledResult | TaskRunErrorResult | TaskRunBackoffResult | TaskRunProgressResult @@ -429,6 +431,15 @@ export namespace TaskRunResult { }; } /** + * Task has been cancelled, discard result and + * do not store error. + */ + export function cancelled(): TaskRunResult { + return { + type: TaskRunResultType.Cancelled, + }; + } + /** * Task is waiting for something, should be invoked * again with exponentiall back-off until some other * result is returned. @@ -499,6 +510,10 @@ export interface TaskRunFinishedResult { type: TaskRunResultType.Finished; } +export interface TaskRunCancelledResult { + type: TaskRunResultType.Cancelled; +} + export interface TaskRunBackoffResult { type: TaskRunResultType.Backoff; } diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -105,7 +105,6 @@ import { TalerMerchantInstanceHttpClient, TalerPreciseTimestamp, TalerUriAction, - TalerUris, TemplateType, TokenUseSig, Transaction, @@ -1815,7 +1814,11 @@ async function checkPaymentByProposalId( type: TalerUriAction.Pay, merchantBaseUrl: purchaseRec.merchantBaseUrl as HostPortPath, // FIXME: change record type orderId: purchaseRec.orderId, - sessionId: sessionId ?? purchaseRec.lastSessionId ?? purchaseRec.downloadSessionId ?? "", + sessionId: + sessionId ?? + purchaseRec.lastSessionId ?? + purchaseRec.downloadSessionId ?? + "", claimToken: purchaseRec.claimToken, }); @@ -2217,7 +2220,7 @@ export async function preparePayForTemplate( req: PreparePayTemplateRequest, ): Promise<PreparePayResult> { const parsedUri = parsePayTemplateUri(req.talerPayTemplateUri); - + if (!parsedUri) { throw Error("invalid taler-template URI"); } @@ -3442,7 +3445,7 @@ async function processPurchasePay( /** Start index of processed outpok tokens. */ let outTokOffset = 0; - logger.info(`have slates: ${slates?.length}`); + logger.trace(`have slates: ${slates?.length}`); let tokenSigs: SignedTokenEnvelope[] | undefined; if (payInfo.slateTokenSigs) { diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -1318,7 +1318,6 @@ export async function processRefreshGroup( `processing refresh sessions for ${refreshGroup.oldCoinPubs.length} old coins`, ); let errors: TalerErrorDetail[] = []; - let inShutdown = false; // Process refresh sessions in sequence. // In the future, we could parallelize request, in particular when multiple @@ -1331,12 +1330,14 @@ export async function processRefreshGroup( try { await processRefreshSession(wex, refreshGroupId, i); } catch (x) { + if (wex.cancellationToken.isCancelled) { + return TaskRunResult.cancelled(); + } if (x instanceof CryptoApiStoppedError) { - inShutdown = true; - logger.info( + logger.trace( "crypto API stopped while processing refresh group, probably the wallet is currently shutting down.", ); - break; + return TaskRunResult.cancelled(); } const err = getErrorDetailFromException(x); logger.warn(`exception in refresh session: ${j2s(err)}`); @@ -1344,10 +1345,6 @@ export async function processRefreshGroup( } } - if (inShutdown) { - return TaskRunResult.finished(); - } - const ctx = new RefreshTransactionContext(wex, refreshGroupId); // We've processed all refresh session and can now update the diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts @@ -543,6 +543,9 @@ export class TaskSchedulerImpl implements TaskScheduler { await this.wait(taskId, info, delay); break; } + case TaskRunResultType.Cancelled: + logger.trace(`task ${taskId} cancelled`); + break; default: assertUnreachable(res); }