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:
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);
}