feat: add standard CLI mode with JSON output for AI agents and automation#921
feat: add standard CLI mode with JSON output for AI agents and automation#921parsoncryptoai wants to merge 22 commits intotronprotocol:developfrom
Conversation
Add a non-interactive standard CLI framework (--output json, --private-key, --mnemonic flags) alongside the existing interactive REPL. Includes a bash harness that verifies all 120+ commands across help, text, JSON, on-chain transactions, REPL parity, and wallet management (321 tests, 315 pass, 0 fail). Key changes: - New cli/ package: StandardCliRunner, OutputFormatter, CommandRegistry, and per-domain command files (Query, Transaction, Staking, etc.) - JSON mode suppresses stray System.out/err from WalletApi layer so only structured OutputFormatter output reaches stdout - Remove debug print from AbiUtil.parseMethod() that contaminated stdout - Harness scripts (harness/) for automated three-way parity verification - Updated .gitignore for runtime artifacts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ments Rename harness/ → qa/ across shell scripts, Java classes, docs, and build config. Fix vote-witness QA test that extracted keystore address instead of witness address by filtering "keystore" lines from list-witnesses output. Add lock/unlock commands, improve GlobalOptions parsing, and update CLAUDE.md baseline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d auth options Move JSON stream suppression earlier in StandardCliRunner to cover network init and authentication (not just command execution), remove --private-key and --mnemonic global options, and update QA/plan docs to reflect current test baseline (321 tests, 314 passed, 1 failed, 6 skipped). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simplify protobuf formatting to use Utils.formatMessageString for both modes, suppress info messages in JSON mode, and update spec/plan docs to clarify the strict JSON-only contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add text+JSON parity, JSON field validation, round-trip verification (set then get-active-wallet), and error case tests (no args, both args, invalid address) for wallet management commands.
Replace login/logout/backup/export commands with list-wallet, set-active-wallet, and get-active-wallet for multi-wallet support. Implement transfer-usdt with automatic energy estimation. Update CLAUDE.md docs and add QA tests for new transaction commands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip unit tests and QA verification when no code files (src/, build.gradle, *.java) have changed. Consolidates two separate hook blocks into one.
… to JSON errors WalletApi.listWallets filtered to skip the .active-wallet config file, which was incorrectly treated as a wallet keystore. OutputFormatter.error() and usageError() now include "success": false in JSON output for consistent envelope format.
|
This PR is currently under review. |
…defaults
- OutputFormatter.success() now wraps jsonData in {"success":true,"data":{...}}
envelope for consistent API response format
- OutputFormatter.protobuf() outputs single-line valid JSON via
JsonFormat.printToString in JSON mode (was using formatJson which
introduced illegal newlines inside string values)
- deploy-contract defaults token-id to empty string (matching REPL behavior)
and origin-energy-limit to 1 (TRON requires > 0)
- add --force support for reset-wallet and clear-wallet-keystore - de-interactivize standard CLI transaction signing with permission-id support - align wallet command JSON schema with success/data envelope - add standard CLI change-password command and QA coverage - improve QA command counting, skip reasons, and stale result cleanup - add --case support to qa/run.sh for targeted reruns - strengthen transaction QA side-effect checks - make send-coin JSON return txid and verify send-coin-balance via tx receipt fallback - update QA reports and fix report documentation
|
Right now the output json does not implement the unified envelope as described in the PR summary. Only success() emits {"success": true, "data": ...}; result() emits {success, message}, and printMessage()/raw()/keyValue() bypass the envelope entirely. This affects real commands, not just edge cases: get-account still uses printMessage(), register-wallet uses raw(), and many transaction/contract commands use result(). In its current state, JSON mode does not provide a stable machine-parseable contract.
|
|
It says the active wallet config is stored at Wallet/active_wallet.conf, but the implementation actually uses Wallet/.active-wallet (ActiveWalletConfig.java (line 15)).
|
| CommandCapture cap = new CommandCapture(); | ||
| cap.startCapture(); | ||
| try { | ||
| String[] cliArgs = {"--network", network, "--mnemonic", mnemonic, cmdName}; |
There was a problem hiding this comment.
Passing the private key / mnemonic as process arguments is a serious security issue. In QARunner, values are constructed into CLI args like --private-key <key> and --mnemonic <words>, which makes them visible via process listings such as ps on Linux/macOS. That is not acceptable on shared machines or in CI environments. These secrets should be passed via environment variables or stdin instead of command-line arguments. (QARunner.java:130, 144, 301)
There was a problem hiding this comment.
I don’t think the ps exposure concern applies here. QARunner does not spawn a subprocess with --private-key/--mnemonic; it passes an in-memory String[] directly into GlobalOptions.parse() within the same JVM. That said, this QA path is stale and should be cleaned up separately because it still assumes old global auth flags.
There was a problem hiding this comment.
@gummy789j Agreed. QARunner does not expose these values via ps. However, from a secret-handling perspective, this approach is still not ideal: both values are kept as Strings in process memory and could be exposed via heap dumps, crash artifacts, or debugging tools.
Also, it looks like the entire QARunner.java file was newly introduced in this PR. Since it’s described as “this QA path is stale”, would it make sense to clean it up as part of this PR?
Additionally, for external calls like qa/config.sh that run java -jar ... --private-key/--mnemonic, there is still a real risk of leaking secrets via ps.
Repro steps
- In terminal A, start a polling
pswatcher:
while true; do
ps axww -o pid=,command= | grep '[w]allet-cli.jar' | grep 'import-wallet'
sleep 0.05
done
- In terminal B, run:
bash qa/run.sh verify
Then paste the private key when prompted:
$ bash qa/run.sh verify
TRON_TEST_APIKEY not set. Please enter your Nile testnet private key:
*****THE_ACTUAL_PRIVATE_KEY******
TRON_TEST_MNEMONIC not set (optional). Mnemonic tests will be skipped.
=== Wallet CLI QA — Mode: verify, Network: nile ===
Building wallet-cli...
Build complete.
Phase 1: Setup & connectivity check...
✓ nile connectivity OK
Standard CLI commands: 118
.........
- You can observe multiple entries with the private key in terminal A:
15456 java -jar build/libs/wallet-cli.jar --network nile import-wallet --private-key *****THE_ACTUAL_PRIVATE_KEY******
The same applies if TRON_TEST_MNEMONIC is set — the mnemonic can also be observed in ps output.
There was a problem hiding this comment.
Agreed that String-based in-memory secret handling is not ideal. For QARunner, though, this is a local, short-lived QA helper running in the same JVM, so I do not view it as the same severity as the external argv/ps exposure.
The concrete reproducible issue here was the QA shell path passing secrets to an external java -jar ... process, and that is fixed now. I am not reworking QARunner’s internal in-memory secret handling in this PR because that would be a broader QA/auth handoff redesign rather than a localized fix.
There was a problem hiding this comment.
@gummy789j This leaves qa/run.sh java-verify on a stale code path. QARunner still builds --private-key / --mnemonic global args, but GlobalOptions no longer supports those flags. In practice, the Java-side verifier is no longer exercising the intended authenticated CLI flows, so its results are unreliable as regression coverage. This is not just dead code: it creates a false sense of QA coverage for the current standard CLI contract.
Reproduction Steps
export TRON_TEST_APIKEY=<your test private key>
export TRON_NETWORK=nileRun:
$ java -cp build/libs/wallet-cli.jar org.tron.qa.QARunner baseline qa/baseline
=== QA Baseline Capture ===
Network: nile
Output dir: qa/baseline
Commands: 118
Capturing: get-address... OK
Capturing: get-balance... OK
Capturing: current-network... OK
Capturing: get-block... OK
Capturing: get-chain-parameters... OK
Capturing: get-bandwidth-prices... OK
Capturing: get-energy-prices... OK
Capturing: get-memo-fee... OK
Capturing: get-next-maintenance-time... OK
Capturing: list-nodes... OK
Capturing: list-witnesses... OK
Capturing: list-asset-issue... OK
Capturing: list-proposals... OK
Capturing: list-exchanges... OK
Capturing: get-market-pair-list... OK
Baseline capture complete: 15 captured, 0 skippedAlthough it prints OK, this does not mean the commands actually succeeded.
In captureBaseline(), the code prints OK unconditionally ([QARunner.java:124]), without checking whether the captured output is a success result or an error.
Root Cause
The issue originates from this line:
String[] cliArgs = {"--network", network, "--private-key", privateKey, cmdName};([QARunner.java:130])
However, GlobalOptions does not support the global flag --private-key ([GlobalOptions.java:48]).
As a result, the parsed arguments are not interpreted as “run get-address with a private key”, but instead:
command = nullcommandArgs = ["--private-key", "<privateKey>", "get-address"]
Then the runner executes:
CommandDefinition cmd = registry.lookup(cmdName);Here, cmdName is null, which eventually triggers:
nameOrAlias.toLowerCase()inside lookup().
Verification
You can verify this by inspecting the captured baseline files:
sed -n '1,40p' qa/baseline/get-address.json
sed -n '1,40p' qa/baseline/current-network.jsonExample outputs:
{
"command": "get-address",
"text_stdout": "User defined config file doesn't exists, use default config file in jar\nError: Cannot invoke \"String.toLowerCase()\" because \"nameOrAlias\" is null\n",
"text_stderr": "",
"json_stdout": "Error: Cannot invoke \"String.toLowerCase()\" because \"nameOrAlias\" is null\n",
"json_stderr": ""
}"text_stdout": "Error: Cannot invoke \"String.toLowerCase()\" because \"nameOrAlias\" is null\n"There was a problem hiding this comment.
Fixed by retirement rather than rehabilitation. QARunner now only keeps the supported list helper; the stale baseline / verify modes have been retired and fail with a clear unsupported/deprecation message, and qa/run.sh java-verify now does the same instead of pretending to provide current regression coverage.
| byte[] priKey = ByteArray.fromHexString(opts.getString("private-key")); | ||
| String walletName = opts.has("name") ? opts.getString("name") : "mywallet"; | ||
|
|
||
| ECKey ecKey = ECKey.fromPrivate(priKey); | ||
| WalletFile walletFile = Wallet.createStandard(passwd, ecKey); |
There was a problem hiding this comment.
Missing Arrays.fill(priKey, (byte) 0) here, so the private key bytes remain in memory until GC, which unnecessarily increases secret exposure.
fixed, still working on rest of comments. |
Wipe temporary secret buffers in the standard CLI import-wallet path after keystore creation. - add try/finally around private-key import flow - clear private key bytes with Arrays.fill(priKey, (byte) 0) - clear derived password bytes after use for consistency with existing secret handling
Good catch. The implementation uses Wallet/.active-wallet; the PR description will be updated to match the actual behavior. |
lxcmyf
left a comment
There was a problem hiding this comment.
PR Code Review
Reviewed the new Standard CLI mode implementation. Found 6 critical, 4 high, and 4 medium severity issues. Key concerns:
- Fund Safety:
register-walletdoesn't return mnemonic;--networksilently drops missing values; auto-confirm stdin bypasses safety prompts - Security: Password/key byte arrays not zeroed after use;
MASTER_PASSWORDbypasses password strength checks - Correctness:
System.exit()inOutputFormatterbypassesfinallycleanup and makes code untestable - Silent Failures:
ActiveWalletConfigswallows all exceptions;authenticate()fails silently with no feedback
See inline comments for details.
| ActiveWalletConfig.setActiveAddress(address); | ||
| out.raw("Register a wallet successful, keystore file name is " + keystoreName); | ||
| } else { | ||
| out.error("register_failed", "Register wallet failed"); |
There was a problem hiding this comment.
[Critical - Fund Safety] register-wallet only outputs the keystore filename. The mnemonic phrase generated by wrapper.registerWallet() is never returned to the user.
If the user loses the keystore file, they cannot recover the wallet. For a cryptocurrency wallet, the mnemonic must be surfaced in both text and JSON output so the user can back it up.
There was a problem hiding this comment.
I am not changing this in the current PR. The standard CLI currently matches the existing REPL behavior by writing the keystore and encrypted mnemonic artifacts rather than printing the mnemonic to stdout/JSON. Changing that would be a product and secret-handling behavior change, so I would rather scope it separately.
|
|
||
| /** Print an error for usage mistakes and exit with code 2. */ | ||
| public void usageError(String message, CommandDefinition cmd) { | ||
| if (mode == OutputMode.JSON) { |
There was a problem hiding this comment.
[Critical - Correctness] System.exit(1) here terminates the JVM before the finally block in StandardCliRunner.execute() can restore System.in, System.out, and System.err.
In JSON mode, stdout/stderr are redirected to /dev/null — the finally cleanup at StandardCliRunner:107-113 is bypassed. Also makes the code completely untestable.
Same issue in usageError() (line 175) and result() (line 105).
Suggestion: Throw a CliExitException(int exitCode) instead. Only call System.exit() once at the main() entry point.
There was a problem hiding this comment.
Fixed. OutputFormatter no longer calls System.exit(). It now signals abort via CliAbortException, StandardCliRunner.execute() maps that to the final exit code, and stream restoration stays in finally. I also added a focused regression test for the in-process runner path.
| } | ||
|
|
||
| // Load specific wallet file and authenticate | ||
| byte[] password = StringUtils.char2Byte(envPwd.toCharArray()); |
There was a problem hiding this comment.
[Critical - Security] The password byte array is never zeroed after use. If checkPassword() throws or the method exits early, password bytes remain in memory.
Suggestion: Wrap lines 150-161 in a try/finally that calls Arrays.fill(password, (byte) 0) on failure paths.
There was a problem hiding this comment.
Fixed. The auto-auth password handling is now wrapped in try/finally, and the temporary byte[] is cleared with Arrays.fill(password, (byte) 0) after use.
| if (i + 1 < args.length) opts.output = args[++i]; | ||
| break; | ||
| case "--network": | ||
| if (i + 1 < args.length) opts.network = args[++i]; |
There was a problem hiding this comment.
[Critical - Fund Safety] If --network is the last argument (no value follows), it is silently ignored and the default network is used.
Worse: wallet-cli --network send-coin --to TXyz --amount 100 consumes send-coin as the network name, leaving the actual command as null.
Running a transaction on the wrong network (mainnet vs testnet) could mean real money lost.
Suggestion: Throw IllegalArgumentException when the required value for --output, --network, --wallet, or --grpc-endpoint is missing.
Same issue on lines 60, 66, 69.
There was a problem hiding this comment.
Fixed. Global option parsing is now strict for --output, --network, --wallet, and --grpc-endpoint: missing values fail fast, and --network / --output also validate allowed values so command tokens are no longer consumed as option values.
| // "1\n" — wallet file selection (choose first) | ||
| // "y\n" — additional signing confirmations | ||
| // Repeated to cover multiple rounds of signing prompts. | ||
| String autoInput = "y\n1\ny\ny\n1\ny\ny\n1\ny\ny\n"; |
There was a problem hiding this comment.
[Critical - Fund Safety] This hardcoded auto-confirm stream blindly answers "y" to all interactive prompts and always selects wallet #1.
- Auto-confirms dangerous operations (e.g., "Are you sure you want to delete this wallet?").
- Users with multiple wallets cannot control which one signs the transaction.
- If the legacy code adds/removes a prompt, this silently feeds wrong answers.
Suggestion: Short-circuit interactive prompts at a higher level (e.g., the PERMISSION_ID_OVERRIDE pattern) rather than faking stdin.
There was a problem hiding this comment.
Agreed on the risk. I am not changing this in the current PR because removing that compatibility path safely would require reworking shared REPL / legacy interaction behavior, and I do not want to take that larger behavioral change as part of this review-fix scope.
|
|
||
| public void add(CommandDefinition cmd) { | ||
| commands.put(cmd.getName(), cmd); | ||
| aliasToName.put(cmd.getName(), cmd.getName()); |
There was a problem hiding this comment.
[Medium] Primary name is stored as-is here, but aliases are lowercased on line 17. lookup() normalizes input to lowercase.
If a command name ever contains uppercase letters, lookup would fail to match the primary name entry.
Suggestion: Normalize here too: aliasToName.put(cmd.getName().toLowerCase(), cmd.getName());
There was a problem hiding this comment.
Fixed. Primary command names are now normalized to lowercase when registered, so lookup stays consistent with the existing lowercase alias normalization.
| while (i < args.length) { | ||
| String token = args[i]; | ||
|
|
||
| if ("-m".equals(token)) { |
There was a problem hiding this comment.
[Medium] Hard-coded -m → "multi" mapping leaks a domain-specific concern into the general-purpose parser. All commands implicitly accept -m, even those that don't use multi-signature mode.
If a command wanted -m for --memo, it would collide.
Suggestion: Add a shortFlag field to OptionDef so individual commands declare their own short flags.
There was a problem hiding this comment.
Fixed. -m is no longer treated as a global shorthand for every command. The parser now only accepts -m for commands that actually declare the multi option.
| WalletApi.setApiCli(WalletApi.initApiCli()); | ||
| break; | ||
| default: | ||
| formatter.usageError("Unknown network: " + network |
There was a problem hiding this comment.
[Medium] The default case calls formatter.usageError() which calls System.exit(2). But there is no return or throw statement after it. If System.exit() is ever replaced with an exception (per the OutputFormatter issue), execution would continue with no network configured.
Also: all four cases share identical WalletApi.setApiCli(WalletApi.initApiCli()) — consider extracting after the switch.
There was a problem hiding this comment.
Fixed as part of the abort-handling refactor. usageError() now throws a controlled abort exception instead of exiting the JVM, so execution does not continue past this branch.
| public boolean isVerbose() { return verbose; } | ||
| public String getCommand() { return command; } | ||
| public String[] getCommandArgs() { return commandArgs; } | ||
|
|
There was a problem hiding this comment.
[Medium] Returns the internal String[] reference directly. Callers can mutate the array and corrupt internal state.
Suggestion: return Arrays.copyOf(commandArgs, commandArgs.length);
There was a problem hiding this comment.
Fixed. getCommandArgs() now returns a defensive copy instead of exposing the internal array directly.
| return null; | ||
| } | ||
| try (FileReader reader = new FileReader(configFile)) { | ||
| Map map = gson.fromJson(reader, Map.class); |
There was a problem hiding this comment.
[Low] Raw type Map — if the JSON contains {"address": 12345} (number instead of string), the (String) cast on line 35 throws ClassCastException, which is then swallowed by the catch block.
Suggestion: Use Map<String, Object> and validate the type before casting.
There was a problem hiding this comment.
Fixed. The active-wallet config read path now uses typed validation instead of a raw cast, so invalid JSON types are rejected explicitly instead of relying on a swallowed ClassCastException.
| case "--wallet": | ||
| opts.wallet = requireValue(args, ++i, "--wallet"); | ||
| break; | ||
| case "--grpc-endpoint": | ||
| opts.grpcEndpoint = requireValue(args, ++i, "--grpc-endpoint"); | ||
| break; |
There was a problem hiding this comment.
--wallet and --grpc-endpoint are dead global flags. They are documented in help and parsed into GlobalOptions, but StandardCliRunner never consumes them.
There was a problem hiding this comment.
Fixed. StandardCliRunner now consumes both global flags: --wallet is used to resolve the auto-auth keystore target, and --grpc-endpoint overrides the ApiClient for the current run.
|
Run |
| return; | ||
| } | ||
|
|
||
| String activeAddress = ActiveWalletConfig.getActiveAddressStrict(); |
There was a problem hiding this comment.
list-wallet now uses getActiveAddressStrict(), so a malformed Wallet/.active-wallet prevents the command from listing any keystores. Since set-active-wallet requires an address or name, this also removes the main in-CLI recovery path for repairing wallet selection. list-wallet should treat an unreadable active-wallet config as "no active wallet" and still enumerate the available wallets.
There was a problem hiding this comment.
Fixed. list-wallet now uses the lenient active-wallet read path instead of the strict one, so a malformed Wallet/.active-wallet is treated as no active wallet and the command still enumerates the available keystores.
| } | ||
| File targetFile = ActiveWalletConfig.findWalletFileByAddress(activeAddress); | ||
| if (targetFile == null) { | ||
| throw new IllegalStateException( |
There was a problem hiding this comment.
authenticate() runs before every command and now throws if .active-wallet points at a missing keystore. That means even wallet-independent commands such as current-network, list-witnesses, or recovery commands like import-wallet fail with execution_error as soon as the selected file is moved or deleted. Auto-login should be skipped or deferred for commands that do not require an authenticated wallet.
There was a problem hiding this comment.
Fixed. The standard CLI no longer runs auto-login unconditionally before every command. It now applies command-level auto-auth policy, so wallet-independent / recovery commands skip auto-login while wallet-required commands still authenticate deterministically.
|
ReproductionRun: java -jar build/libs/wallet-cli.jar \
--output json \
--network nile \
trigger-constant-contract \
--contract TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf \
--method "balanceOf(address)" \
--params "\"TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf\""Result:
Additionally the QA suite still does not implement the “semantic comparison for format-independent validation” described in the summary.
|
| public class InteractiveSession { | ||
|
|
||
| private final Object clientInstance; | ||
|
|
||
| public InteractiveSession(Object clientInstance) { |
There was a problem hiding this comment.
Unused code — can the entire file be removed?
There was a problem hiding this comment.
Removed. InteractiveSession was unused, and I deleted it as part of retiring the stale Java-side QA verification path.
| * check_json_text_parity and filter_noise functions, used by | ||
| * QARunner for Java-side verification. | ||
| */ | ||
| public class TextSemanticParser { |
There was a problem hiding this comment.
Unused code - should be used in runVerification() (QARunner.java (line 175) )?
There was a problem hiding this comment.
I removed this instead of wiring it back in. The supported QA paths are now qa/run.sh verify (optionally with --query-batch) and QARunner list; I do not want to reintroduce a second unused semantic/parser path in Java.
| public static boolean isNumericallyEquivalent(String sunValue, String trxValue) { | ||
| try { | ||
| long sun = Long.parseLong(sunValue.replaceAll("[^0-9]", "")); | ||
| double trx = Double.parseDouble(trxValue.replaceAll("[^0-9.]", "")); |
There was a problem hiding this comment.
Although this is currently dead code, it's still worth noting that using floating-point for SUN/TRX conversion introduces precision issues.
There was a problem hiding this comment.
Agreed. I removed TextSemanticParser entirely in this cleanup, so this floating-point precision issue no longer exists in the codebase.
- add command-level auto-auth policy for standard CLI - keep list-wallet usable when active wallet config is malformed - retire stale QARunner baseline/verify modes - remove unused QA helper code - fix remaining parser/config review follow-ups
| .mapToLong(org.tron.trident.proto.Response.ChainParameters.ChainParameter::getValue) | ||
| .findFirst() | ||
| .orElse(420L); | ||
| long feeLimit = (long) (energyFee * energyUsed * 1.2); |
There was a problem hiding this comment.
There is a potential long overflow issue.
energyFee * energyUsed is a long × long multiplication. When the product exceeds Long.MAX_VALUE, it overflows to a negative value, leading to an incorrect fee limit.
There was a problem hiding this comment.
Fixed.
The fee-limit calculation no longer uses unchecked long * long or floating-point arithmetic. I moved it to a shared helper (WalletApiWrapper.computeBufferedFeeLimit) that uses Math.multiplyExact / Math.addExact with a 20% integer buffer. Both the standard CLI and the legacy REPL transfer-USDT path now use the same exact-arithmetic calculation, and overflow fails fast instead of silently wrapping.
| char[] envPassword = resolveEnvPassword(System.getenv("MASTER_PASSWORD"), checkStrength); | ||
| if (envPassword != null) { | ||
| return envPassword; | ||
| } |
There was a problem hiding this comment.
Utils.inputPassword() globally prioritizes reading from the MASTER_PASSWORD environment variable.
As a result, if a user runs the REPL interactive mode in a terminal where MASTER_PASSWORD is set, any password prompt that goes through inputPassword() will be automatically satisfied by the environment variable, leaving no opportunity for manual input.
Additionally, when an interactive command enforces strong password validation, if MASTER_PASSWORD does not meet the policy, the command will fail immediately instead of prompting the user again for input.
There was a problem hiding this comment.
Fixed.
Utils.inputPassword() no longer unconditionally prioritizes MASTER_PASSWORD. The env-password fallback is now enabled only within the standard CLI runner scope, so non-interactive standard CLI behavior is preserved while the original interactive REPL path goes back to prompt-first behavior instead of being silently overridden by the environment.
| byte[] passwd = org.tron.keystore.StringUtils.char2Byte( | ||
| envPassword.toCharArray()); | ||
| List<String> words = Arrays.asList( | ||
| opts.getString("mnemonic").split("\\s+")); | ||
| String walletName = opts.has("name") ? opts.getString("name") : "mywallet"; | ||
|
|
||
| byte[] priKey = MnemonicUtils.getPrivateKeyFromMnemonic(words); |
There was a problem hiding this comment.
passwd is not cleared, and priKey is not cleared at the appropriate point. It should follow the approach used in import-wallet (lines 102–104), clearing sensitive data in a try/finally block:
Arrays.fill(priKey, (byte) 0);
Arrays.fill(passwd, (byte) 0);There was a problem hiding this comment.
Fixed. import-wallet-by-mnemonic now uses a try/finally cleanup path so both priKey and passwd are cleared on all exit paths, including exceptions.
| char[] password = envPassword.toCharArray(); | ||
| String keystoreName = wrapper.registerWallet(password, wordCount); |
There was a problem hiding this comment.
A try/finally block should be added here to clear the char[] password, since wrapper.registerWallet clears a different variable (byte[] passwd).
There was a problem hiding this comment.
Fixed. register-wallet now wraps the local char[] password in a try/finally and clears it after wrapper.registerWallet(...) returns or throws. This is separate from the wrapper's internal byte[] cleanup.
- route trigger-constant-contract through OutputFormatter so --output json always emits an envelope - move constant-call normalization into WalletApi to avoid duplicated logic in ContractCommands - add typed command errors for trigger-constant-contract, gas-free-info, and gas-free-trace - fix gas-free-info / gas-free-trace standard CLI error envelopes - return auth_required instead of false success for tronlink-multi-sign auth failures - extend QA coverage for trigger-constant-contract json parity - fix gas-free-trace QA case to use --id in shell and batch runner - strengthen semantic parity checks for text/json validation
Confirmed and fixed.
I also addressed the QA gap called out here:
Verified with:
This now passes, and the JSON-mode empty-output regression is no longer reproducible. |
Confirmed and fixed. |
| if (def != null && def.getType() == OptionDef.Type.BOOLEAN) { | ||
| // If next arg doesn't look like a flag value, treat as boolean flag | ||
| if (i + 1 >= args.length || args[i + 1].startsWith("--") || args[i + 1].startsWith("-")) { | ||
| isBooleanFlag = true; | ||
| } | ||
| } |
There was a problem hiding this comment.
CommandDefinition.parseArgs() is too permissive when handling BOOLEAN options with a “flag without value”.
The current implementation treats any following token that starts with - as the next option. As a result, if a boolean option is explicitly given a value that starts with - (such as -1), it will be incorrectly parsed as true, and the token will be left for the next round, eventually triggering an Unexpected argument.
This does not affect typical numeric parameters such as --amount -1, but it makes the parsing behavior for BOOLEAN options less robust.
It is recommended to either:
- Only treat tokens with a
--prefix as the next long option, or - Enforce that BOOLEAN options support only two forms: a valueless flag, or explicit
true|false, and tighten the parsing logic accordingly.
There was a problem hiding this comment.
Fixed. BOOLEAN options now accept only --flag or explicit boolean values (true/false/1/0/yes/no). Values like -1 are no longer treated as an implicit valueless flag and now fail with a clear usage error. I also added a focused parser test covering this case.
- route trigger-constant-contract through OutputFormatter and return stable JSON envelopes - move constant-call normalization into shared WalletApi/WalletApiWrapper logic - add typed command-error handling for trigger-constant-contract, gas-free-info, and gas-free-trace - fix tronlink-multi-sign auth failures to return an error envelope instead of false success - scope MASTER_PASSWORD fallback to standard CLI so REPL password prompts keep legacy behavior - harden transfer-usdt fee-limit calculation with exact arithmetic and overflow protection in both CLI and REPL - clear wallet command sensitive buffers with try/finally cleanup - tighten BOOLEAN option parsing to avoid implicit flag/value ambiguity - retire stale Java QA verification paths and fix QA semantic-parity / query-runner regressions - extend tests and QA coverage for JSON parity and command error handling
| // Determine whether this is a boolean flag (no following value) | ||
| boolean isBooleanFlag = i + 1 >= args.length || args[i + 1].startsWith("--"); | ||
| if (isBooleanFlag) { | ||
| values.put(key, "true"); | ||
| i++; |
There was a problem hiding this comment.
CommandDefinition.parseArgs() treats non-boolean options without a value as a flag and assigns "true", effectively bypassing the “missing required option” check and deferring what should be a usage error into a type error or invalid address error.
For example, --contract --method balanceOf(address) ends up parsing contract=true instead of immediately reporting a missing value for --contract. This then leads to secondary errors in ParsedOptions.java (line 38) / (line 68).
| File walletDir = resolveWalletDir(); | ||
| if (!walletDir.exists() || !walletDir.isDirectory()) { | ||
| formatter.info("No wallet directory found — skipping auto-login"); | ||
| return; // No wallet — commands that need auth will fail gracefully |
There was a problem hiding this comment.
Even when --wallet explicitly points to a keystore, it still doesn’t work in a “clean directory”.
authenticate() first requires the local Wallet/ directory to exist and contain at least one .json file before it even checks resolveAuthenticationWalletFile->globalOpts.getWallet(). This means that running:
wallet-cli --wallet /abs/path/to/keystore.json get-addresswill skip auto-login if the current working directory has no Wallet/, instead of using the user-provided keystore file.
| byte[] passwd = char2Byte(password); | ||
|
|
||
| WalletFile walletFile = WalletApi.CreateWalletFile(passwd, wordsNumber); | ||
| nameWallet(walletFile, false); | ||
| clear(passwd); | ||
|
|
||
| String keystoreName = WalletApi.store2Keystore(walletFile); | ||
| logout(); | ||
| return keystoreName; | ||
| } | ||
|
|
||
| public String importWallet(char[] password, byte[] priKey, List<String> mnemonic) throws CipherException, IOException { | ||
| if (!WalletApi.passwordValid(password)) { | ||
| return null; | ||
| } | ||
| if (!WalletApi.priKeyValid(priKey)) { | ||
| return null; | ||
| } | ||
|
|
||
| byte[] passwd = char2Byte(password); | ||
|
|
||
| WalletFile walletFile = WalletApi.CreateWalletFile(passwd, priKey, mnemonic); | ||
| nameWallet(walletFile, false); | ||
| clear(passwd); |
There was a problem hiding this comment.
Clearing byte[] passwd only on the success path is insufficient. If registerWallet() / importWallet() throws an exception in WalletApi.CreateWalletFile(...) or nameWallet(...), passwd will remain in memory until GC. It’s still recommended to clear it in a try/finally block.
| wrapper.viewTransactionHistory(); | ||
| out.result(true, "Transaction history completed", "Transaction history failed"); | ||
| }) | ||
| .build()); | ||
| } | ||
|
|
||
| private static void registerViewBackupRecords(CommandRegistry registry) { | ||
| registry.add(CommandDefinition.builder() | ||
| .name("view-backup-records") | ||
| .aliases("viewbackuprecords") | ||
| .description("View backup records") | ||
| .handler((opts, wrapper, out) -> { | ||
| wrapper.viewBackupRecords(); | ||
| out.result(true, "Backup records completed", "Backup records failed"); |
There was a problem hiding this comment.
view-transaction-history and view-backup-records falsely report success in the CLI when not logged in.
The handlers unconditionally call out.result(true, ...), while the wrapper only prints a warning and returns early. In JSON mode, the warning is suppressed, so the caller ends up seeing success: true.
See WalletApiWrapper.java (line 2056-2058) and (line 2067-2069).
| if (!args[i].startsWith("--")) { | ||
| opts.command = args[i].toLowerCase(); | ||
| commandFound = true; | ||
| } else { | ||
| // Unknown global flag — treat as start of command args | ||
| remaining.add(args[i]); | ||
| commandFound = true; | ||
| } |
There was a problem hiding this comment.
GlobalOptions.parse() silently treats unknown global flags as the start of command arguments instead of rejecting them. For an input like --outputt json get-balance, opts.command remains null while the unknown flag is pushed into commandArgs, which later drives StandardCliRunner into a null command lookup path instead of producing a clear “unknown global option: --outputt” usage error. This is misleading for users and can surface as a downstream execution error rather than a parser-level diagnostic.
| String[] parts = votesStr.trim().split("\\s+"); | ||
| if (parts.length % 2 != 0) { | ||
| out.usageError("Votes must be pairs of 'address count'", null); | ||
| return; | ||
| } | ||
| HashMap<String, String> witness = new HashMap<String, String>(); | ||
| for (int i = 0; i < parts.length; i += 2) { | ||
| witness.put(parts[i], parts[i + 1]); | ||
| } |
There was a problem hiding this comment.
It only validates that --votes contains an even number of tokens, but does not validate each vote count at the CLI boundary. As a result, malformed counts such as non-numeric values abc are deferred to lower layers (Long.parseLong(...) in WalletApi), and negative/zero counts are only rejected on the multi=true path. This should be normalized into a parser-level usage error with explicit “count must be a positive integer” feedback.
| private final String code; | ||
|
|
||
| public CommandErrorException(String code, String message) { | ||
| super(message); |
There was a problem hiding this comment.
CommandErrorException is being used as a structured command-flow exception, similar to CliAbortException, but it still pays the full fillInStackTrace() cost via super(message). Since callers immediately catch it and convert it into structured CLI output, the stack trace is not used for diagnostics. Consider disabling writable stack traces here as well, e.g. super(message, null, true, false), to keep the exception aligned with its control-flow role.
Summary
java -jar wallet-cli.jar --network nile get-account --address TXyz...) alongside the existingREPL mode. Supports
--output jsonfor structured output,--networkfor network selection, and--quietflag. Designed for AI agents, scripts, andCI/CD pipelines.
CommandRegistry/CommandDefinitionpattern with fluent builder API. Commands organized by domain incli/commands/(Query,Transaction, Wallet, Staking, Contract, Exchange, Proposal, Witness, Misc). Supports aliases, typed options, and fuzzy command suggestion on typos.
{"success":true,"data":...}or{"success":false,"error":"..."}in JSON mode, with stdout/stderrsuppression to guarantee machine-parseable output.
set-active-wallet/list-walletscommands, stored inWallet/active_wallet.conf.qa/) comparing REPL vs standard CLI output across text and JSON modes, with semantic comparisonfor format-independent validation.
transfer-usdtcommand for TRC20 USDT transfers with automatic contract address resolution per network.Test plan
./gradlew buildjava -jar build/libs/wallet-cli.jar --network nile get-account --address <addr>java -jar build/libs/wallet-cli.jar --output json --network nile get-account --address <addr>./gradlew runTRON_TEST_APIKEY=<key> bash qa/run.sh verifyset-active-wallet,list-walletstransfer-usdt --to <addr> --amount 1