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 33 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.
| registry.add(noAuthCommand() | ||
| .name("reset-wallet") |
There was a problem hiding this comment.
reset-wallet, when configured as a noAuthCommand(), becomes a highly destructive local cleanup command that can delete all wallets with --force without requiring authentication.
With --force, it directly calls resetWalletForCli(force) in WalletApiWrapper.java (line 2356), which eventually invokes ClearWalletUtils.deleteFilesQuiet(filePaths) in WalletApiWrapper.java (line 2410) to delete local wallet/mnemonic files.
| String privateKey = Hex.toHexString(credentials.getPair().getPrivateKey()); | ||
| String ledgerPath = getLedgerPath(passwd, wf); | ||
| boolean isLedgerFile = wf.getName().contains("Ledger"); |
There was a problem hiding this comment.
gasFreeTransferInternal() currently converts the private key into a non-clearable hex string, while the finally block only clears the password and not this copy of the private key. Worse, the string is created before checking isLedgerFile, so even the Ledger path unnecessarily retains an extra copy of the private key.
It is recommended to defer generating local signing material until the non-Ledger branch, keep it as a clearable byte[] where possible, and ensure it is cleared in a finally block.
| if (multi) { | ||
| if (!DecodeUtil.addressValid(owner) || !DecodeUtil.addressValid(to)) { | ||
| return false; | ||
| } | ||
| if (Arrays.equals(to, owner)) { | ||
| return false; | ||
| } | ||
| if (amount <= 0) { | ||
| return false; | ||
| } | ||
| Response.Account account = queryAccount(owner); | ||
| if (account == null) { | ||
| recordLastCliOperationError("Failed to query account."); | ||
| return false; | ||
| } | ||
| long balance = account.getBalance(); | ||
| if (balance < amount) { | ||
| return false; | ||
| } | ||
| if (balance - amount < 200_0000L) { | ||
| return false; | ||
| } | ||
| if (!isControlledForCli(owner)) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
In the multisig branch of sendCoinForCli, there are still six return false paths that do not call recordLastCliOperationError(), causing the standard CLI to end up showing only the generic SendCoin failed !! without indicating the specific reason. Only the queryAccount(owner) == null case has been updated to record a clear error.
| private static Pair<Pair<String, Boolean>, Pair<String, Boolean>> customNodes; | ||
| public MultiSignService multiSignService = initMultiSignService(); | ||
|
|
||
| private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); |
There was a problem hiding this comment.
WalletApi owns a per-instance ScheduledExecutorService (WalletApi.java line 197) and exposes an explicit cleanup() hook to shut it down (WalletApi.java line 4365), but StandardCliRunner never calls wrapper.cleanup() on exit.
This is a lifecycle management gap rather than a guaranteed “one thread leaked per WalletApi construction” issue: the executor may remain idle unless a scheduled task is actually used, but once it is used, there is no shutdown path in the standard CLI.
It would be safer to either create the scheduler lazily only when needed, or ensure StandardCliRunner.execute() calls wrapper.cleanup() in a finally block.
| private List<WalletFile> walletList = new ArrayList<>(); | ||
| @Getter | ||
| @Setter | ||
| private static ApiClient apiCli = initApiCli(); |
There was a problem hiding this comment.
WalletApi.apiCli is a mutable static client reference updated without synchronization (WalletApi.java lines 188 and 201). This introduces a general concurrency risk if RPC calls race with updateRpcCli()/network switching, since the old client is closed before the shared reference is replaced.
| registry.add(CommandDefinition.builder() | ||
| .authPolicy(CommandDefinition.AuthPolicy.REQUIRE) | ||
| .name("gas-free-info") |
There was a problem hiding this comment.
gas-free-info is currently stricter than comparable query commands: even when --address is provided, it is still hard-coded to AuthPolicy.REQUIRE, and the wrapper also unconditionally requires a logged-in wallet before evaluating the optional address parameter (WalletApiWrapper.java line 2656).
This is inconsistent with commands such as get-balance / estimate-energy, where supplying an address allows no-auth usage.
The fix needs to address both layers:
- Use
authPolicyResolver(opts -> opts.has("address") ? NEVER : REQUIRE)in the command definition - Make
getGasFreeInfoData(address)require login only whenaddressis omitted
|
The project’s cryptographic core paths are still under-tested. There is limited deterministic test coverage for security-sensitive flows such as transaction signing, private-key import, and mnemonic-to-key derivation. Most current tests focus on CLI routing, output envelopes, and wrapper behavior rather than validating known cryptographic vectors end-to-end. Consider adding fixed test vectors for “private key -> address”, “mnemonic -> private key/address”, and representative signed-transaction cases. |
| this.walletFile = null; | ||
| setLedgerUser(false); | ||
| setCredentials(null); | ||
| setUnifiedPassword(null); |
There was a problem hiding this comment.
logout() already sets unifiedPassword to null, but does not zero out the array contents first; pwdForDeploy is not handled at all. The root issue is that sensitive byte arrays are not properly zeroized.
| public static void setActiveAddress(String address) throws IOException { | ||
| File dir = getWalletDir(); | ||
| if (!dir.exists()) { | ||
| dir.mkdirs(); | ||
| } | ||
| File configFile = new File(dir, CONFIG_FILE); | ||
| Map<String, String> data = new LinkedHashMap<String, String>(); | ||
| data.put("address", address); | ||
| try (FileWriter writer = new FileWriter(configFile)) { | ||
| gson.toJson(data, writer); | ||
| } |
There was a problem hiding this comment.
Currently, .active-wallet (ActiveWalletConfig.java line 62) and keystore files (WalletApi.java line 501, WalletUtils.java line 117) are created with the process’s default permissions, which ultimately depend on the host environment’s umask. A safer approach is to explicitly set owner-only permissions, e.g., 0600 for files and 0700 for directories.
| ByteString constantResult = normalized.getConstantResult(0); | ||
| bigInteger = new BigInteger(1, constantResult.toByteArray()); | ||
| } | ||
| return Triple.of(true, energyUsed, bigInteger.longValue()); |
There was a problem hiding this comment.
When calling BigInteger.longValue(), values exceeding Long.MAX_VALUE are silently truncated to the low 64
bits. Use longValueExact() with explicit overflow handling instead, or keep the BigInteger/string
representation as-is. See: WalletApiWrapper.java:1994, WalletApi.java:3698, and WalletApi.java:3772.
| public static String getLastBroadcastTxId() { | ||
| return LAST_BROADCAST_TX_ID.get(); | ||
| } |
There was a problem hiding this comment.
LAST_BROADCAST_TX_ID has a “read-only without clearing” issue in the CLI scenario.
In WalletApi.java (line 1238), only getLastBroadcastTxId() is provided; meanwhile, the command layer reads the txid in CommandSupport.java (line 39) and TransactionCommands.java (line 73) without clearing it.
As a result, the txid remains attached to the thread until a subsequent transaction entry explicitly calls remove() in WalletApi.java (line 1094) / (line 1146). This indicates an overly long ThreadLocal lifecycle.
It is recommended to change to consumeLastBroadcastTxId(), similar to consumeLastCliOperationError(), so that reading also clears the value, enabling one-time consumption.
| if (!Boolean.TRUE.equals(result.getLeft())) { | ||
| throw new CommandErrorException("execution_error", "CallContract failed !!"); | ||
| } |
There was a problem hiding this comment.
When result.getLeft() is false, a generic CommandErrorException is thrown directly without going through throwIfCliOperationFailed(). If the underlying layer has already set LAST_CLI_OPERATION_ERROR, it will not be consumed here, leaving the error state lingering in the thread.
It is recommended to reuse the unified failure-handling logic in this case.
| } | ||
| return activeWalletFile.getCanonicalFile().equals(targetWalletFile.getCanonicalFile()); | ||
| } catch (Exception e) { | ||
| return false; |
There was a problem hiding this comment.
shouldClearActiveWalletForDeletedTarget() swallows all exceptions when determining whether the deleted keystore is the current active wallet and directly returns false, silently downgrading what should be an exposed active-wallet configuration issue into “no cleanup needed”.
| } | ||
| String privateKeyHex = System.getenv(PRIVATE_KEY_ENV); | ||
| if (privateKeyHex == null || privateKeyHex.isEmpty()) { | ||
| out.usageError("import-wallet requires " + PRIVATE_KEY_ENV + " in standard CLI mode.", null); |
There was a problem hiding this comment.
WalletCommands.java (line 99), WalletCommands.java (line 144), and MiscCommands.java (line 65) call out.usageError(...) when parameters or environment variables are missing, but then continue executing business logic. For example:
import-walletproceeds toByteArray.fromHexString(privateKeyHex)import-wallet-by-mnemonicproceeds tomnemonic.trim().split(...)get-private-key-by-mnemonicproceeds tomnemonicStr.trim().split(...)
These paths currently do not execute only because usageError() eventually throws an exception via recordError(). This ties the correctness of the handler to the side effects of OutputFormatter, rather than explicit local control flow. If usageError() is later changed to “record error without throwing,” these locations will immediately become real runtime issues.
Recommendation:
Add an explicit return; after each out.usageError(...) to complete the guard clause. This ensures the handler remains correct and readable even if the formatter’s exception semantics change in the future.
| return 2; // unreachable after usageError() | ||
| } | ||
|
|
||
| applyPermissionIdOverride(cmd, opts); |
There was a problem hiding this comment.
StandardCliRunner sets a global ThreadLocal once via applyPermissionIdOverride(cmd, opts) before command execution. However, multiple handlers also locally call setPermissionIdOverride(permissionId) and clean it up in finally.
This results in “double-setting” the same thread variable. While values are usually the same and no error occurs, the responsibility becomes unclear:
- If the runner is the unified injection layer, then the repeated set/clear in handlers is redundant.
- If handlers are meant to control precise scope, then the runner’s pre-injection expands the scope, making
permission-idno longer limited to the minimal code path that actually requires signing.
For commands like transfer-usdt, which involve two-stage calls, the handler’s try/finally clearly indicates an intent for scoped control. This further shows that runner-level injection and handler-level injection overlap in design.
Recommendation:
Converge responsibilities by choosing one of the following:
- Keep runner-level injection and remove duplicate set/clear logic from handlers; or
- Remove
applyPermissionIdOverride()from the runner and let each handler managepermission-idwithin the minimal required scope.
I recommend the second option, since handlers have better knowledge of where the override is actually needed and can enforce a more precise scope.
References:
StandardCliRunner.java(line 99)StandardCliRunner.java(line 230)TransactionCommands.java(line 53, line 141)StakingCommands.java(line 67)ContractCommands.java(line 125)
| File externalDir = Files.createTempDirectory("change-password-global-wallet-file").toFile(); | ||
| File walletFile = new File(externalDir, "alpha.json"); | ||
| Assert.assertTrue(walletFile.createNewFile()); | ||
| System.setProperty("user.dir", tempDir.getAbsolutePath()); |
There was a problem hiding this comment.
These tests switch the wallet root directory by overriding the process-wide global state via System.setProperty("user.dir", ...). The current usage spans multiple test files, making it inherently unsuitable for parallel execution.
Since user.dir is a JVM-wide shared state rather than test-local state, concurrent test execution can lead to nondeterministic failures and hard-to-reproduce flaky tests.
References:
StandardCliCommandRoutingTest.java(line 27, line 159)StandardCliRunnerTest.java(line 304)ActiveWalletConfigTest.java(line 61)
| PrintStream originalOut = System.out; | ||
| ByteArrayOutputStream stdout = new ByteArrayOutputStream(); | ||
| WalletApi.setCurrentNetwork(NetType.NILE); | ||
| System.setOut(new PrintStream(stdout)); |
There was a problem hiding this comment.
Tests in the following files:
TransactionCommandsTest.java(line 43)StandardCliRunnerTest.java(line 305)ClientMainTest.java(line 16)TransactionUtilsTest.java(line 18)ClearWalletUtilsTest.java(line 20)ActiveWalletConfigTest.java(line 80)
extensively use System.setOut(...) / System.setErr(...) to redirect JVM-global standard streams for capturing output. Similar to the user.dir issue, this is process-wide shared state and inherently unsuitable for parallel execution.
Since System.out / System.err are JVM-wide global objects, concurrent tests can interfere with each other by competing for the same streams, leading to mixed outputs and flaky assertions.
The repository already contains a safer pattern. For example, OutputFormatterTest.java (line 15) injects a PrintStream directly into OutputFormatter, avoiding the need to mutate global streams.
| @Override | ||
| public Triple<Boolean, Long, Long> callContractForCli( | ||
| byte[] ownerAddress, | ||
| byte[] contractAddress, | ||
| long callValue, | ||
| byte[] data, | ||
| long feeLimit, | ||
| long tokenValue, | ||
| String tokenId, | ||
| boolean isConstant, | ||
| boolean display, | ||
| boolean multi) { | ||
| return Triple.of(true, 0L, 0L); |
There was a problem hiding this comment.
StandardCliCommandRoutingTest.java (line 46) and (line 95) only verify that legacy methods are not invoked, but do not explicitly prove that the CLI-safe methods are actually called. In contrast, the overrides in StandardCliCommandRoutingTest.java (line 133) and (line 325) already validate input arguments, which inherently serve as proof of invocation.
| return Arrays.copyOf(wallet.getUnifiedPassword(), wallet.getUnifiedPassword().length); | ||
| } |
There was a problem hiding this comment.
wallet.getUnifiedPassword() repeated getter access on mutable state; prefer caching wallet.getUnifiedPassword() in a local variable before copying. This improves robustness/readability.
| public void sendCoinForCli(byte[] ownerAddress, byte[] toAddress, long amount, boolean multi) { | ||
| requireLoggedInWalletForCli(); | ||
| try { | ||
| throwIfCliOperationFailed( | ||
| wallet.sendCoinForCli(ownerAddress, toAddress, amount, multi), | ||
| "SendCoin failed !!"); | ||
| } catch (IllegalStateException e) { | ||
| throwCliError("execution_error", "SendCoin failed !!", e); | ||
| } catch (Exception e) { | ||
| throwCliError("execution_error", "SendCoin failed !!", e); | ||
| } |
There was a problem hiding this comment.
WalletApiWrapper contains many *ForCli methods that repeat the same try/catch boilerplate pattern:
requireLoggedInWalletForCli() → call underlying wallet.xxxForCli(...) → throwIfCliOperationFailed(...) → catch IllegalStateException / Exception and convert to throwCliError(...).
Examples:
WalletApiWrapper.java(line 884)WalletApiWrapper.java(line 908)WalletApiWrapper.java(line 1250)WalletApiWrapper.java(line 1273)WalletApiWrapper.java(line 1297)WalletApiWrapper.java(line 1897)
Problem
This boilerplate has already spread across dozens of locations, introducing two maintenance costs:
-
Behavior consistency depends on manual duplication
As soon as one method slightly deviates from the template (e.g.,callContractForCli()in line 1931), inconsistencies arise in exception handling / error consumption. -
High cost for future strategy changes
If CLI error mapping, logging, authentication pre-checks, orLAST_CLI_OPERATION_ERRORconsumption semantics need to be updated, changes must be applied across 30+ methods, increasing the risk of omissions.
Recommendation
Extract this pattern into a unified helper, for example:
runCliAction(String failureMessage, CliBooleanAction action)or:
runCliQuery(...) / runCliMutation(...)The helper should be responsible for:
requireLoggedInWalletForCli()- invoking
action throwIfCliOperationFailed(...)- mapping
IllegalStateException/ExceptiontothrowCliError(...)
Then each *ForCli method only needs to forward parameters, e.g.:
public void sendCoinForCli(...) {
runCliAction("SendCoin failed !!", () -> wallet.sendCoinForCli(...));
}| private boolean processTransactionExtentionForCli( | ||
| Response.TransactionExtention transactionExtention, boolean multi) | ||
| throws IOException, CipherException, CancelException { | ||
| LAST_BROADCAST_TX_ID.remove(); |
There was a problem hiding this comment.
The transaction processing flow in WalletApi has clearly diverged into multiple nearly duplicated implementations, resulting in high maintenance cost and increasing the risk of behavioral drift between CLI and non-CLI paths.
Locations
WalletApi.java(line 1037):processTransactionExtention(...)WalletApi.java(line 1091):processTransactionExtentionForCli(...)WalletApi.java(line 1144):processTransactionForCli(...)WalletApi.java(line 1204):processTransaction(...)
Problem
These paths share essentially the same core steps:
- Clear previous state
- Validate transaction / contract
- Validate multi-sign supported contract types
- Sign
- Broadcast
- Record txid / history
The real differences are limited to a few strategy points:
- Input type:
TransactionExtentionvsTransaction - Error handling: CLI vs non-CLI
- Different signing/broadcast functions for CLI vs non-CLI
- Whether to print interactive output
However, these differences are currently implemented via copy-paste rather than a shared skeleton with injected strategies. This has already caused observable drift, for example:
- CLI path introduces
LAST_CLI_OPERATION_ERRORlifecycle management - Non-CLI path has more complete console output
processTransactionForCli(...)andprocessTransactionExtentionForCli(...)are themselves highly duplicated
Recommendation
Extract the common flow into a unified skeleton method, and pass in strategy components (error handling, signing function, broadcast function, output behavior) as parameters or strategy branches.
At minimum, merge:
processTransactionExtention(...)withprocessTransactionExtentionForCli(...)processTransaction(...)withprocessTransactionForCli(...)
This would significantly reduce the need to manually synchronize logic across 3–4 variants and lower the risk of behavioral drift going forward.
| long originEnergyLimit, boolean multi) | ||
| throws CipherException, IOException, CancelException, IllegalException { | ||
| if (wallet == null || !wallet.isLoginState()) { | ||
| System.out.println("Warning: updateSetting " + failedHighlight() + ", Please login first !!"); |
There was a problem hiding this comment.
There are multiple “not logged in” warning messages in WalletApiWrapper that are clearly copy-paste leftovers. The method names have changed, but the error messages still say Warning: updateSetting ....
Locations
WalletApiWrapper.java(line 1815):updateEnergyLimit(...)WalletApiWrapper.java(line 1839):clearContractABI(...)WalletApiWrapper.java(line 2081):updateBrokerage(...)WalletApiWrapper.java(line 2136):marketSellAsset(...)WalletApiWrapper.java(line 2165):marketCancelOrder(...)WalletApiWrapper.java(line 2261):lock()WalletApiWrapper.java(line 2273):unlock(...)
Problem
When not logged in, these methods print messages that do not match the current operation. For example, marketSellAsset(), lock(), and unlock() all output Warning: updateSetting ....
This does not break the main flow, but it directly misleads users and troubleshooters. It also indicates that these error messages are maintained via copy-paste, making future drift likely.
Recommendation
Update the messages to reflect the actual operation name. Additionally, consider extracting a unified helper for “Please login first” warnings to avoid similar copy-paste errors in the future.
| public void add(CommandDefinition cmd) { | ||
| commands.put(cmd.getName(), cmd); | ||
| aliasToName.put(cmd.getName().toLowerCase(), cmd.getName()); | ||
| for (String alias : cmd.getAliases()) { | ||
| aliasToName.put(alias.toLowerCase(), cmd.getName()); |
There was a problem hiding this comment.
CommandRegistry.add() currently does not detect conflicts between command names and aliases. Later registrations silently overwrite earlier ones, so the final registry state depends on registration order.
Location
CommandRegistry.java(line 12)
Problem
add() currently performs:
commands.put(cmd.getName(), cmd)aliasToName.put(cmd.getName().toLowerCase(), cmd.getName())- For each alias:
aliasToName.put(alias.toLowerCase(), cmd.getName())
This means the following conflicts are not detected:
- A new command’s alias conflicts with an existing command name
- A new command’s alias conflicts with an existing alias
- A new command name conflicts with an existing alias
- Duplicate registration of the same command name
As a result, the behavior of lookup / suggest / help depends on “who registers last” instead of failing explicitly. For a CLI that centrally registers all commands, this represents a missing architectural constraint at the registry level—uniqueness should be enforced by the registry itself, rather than relying on manual avoidance of conflicts.
Recommendation
Perform normalized conflict checks during add(), and throw an exception when a conflict is detected, including:
- The conflicting token
- The command that already owns it
- The command being registered
Also add test coverage for:
- alias vs alias
- alias vs primary name
- duplicate primary name
| String envPassword = System.getenv("MASTER_PASSWORD"); | ||
| if (envPassword == null || envPassword.isEmpty()) { | ||
| out.error("auth_required", | ||
| "Set MASTER_PASSWORD environment variable for non-interactive wallet creation"); |
There was a problem hiding this comment.
register-wallet / import-wallet / import-wallet-by-mnemonic return auth_required when the MASTER_PASSWORD environment variable is missing. This mixes with the more common “please login first” semantics in the CLI, making the error code boundary unclear.
Locations
WalletCommands.java(line 56)WalletCommands.java(line 91)WalletCommands.java(line 136)
Problem
The actual failure reason here is:
“missing required input source (MASTER_PASSWORD) in non-interactive CLI mode”,
rather than “session not authenticated” or “login required for an existing wallet”.
In this repository, auth_required is more commonly used for scenarios like:
Please login first !!MASTER_PASSWORD is required for unlock- Other operations that require an authenticated session
Using the same error code for register-wallet makes it difficult for callers to distinguish between:
- Missing environment configuration
- Login required
- Missing unlock credentials
Recommendation
To keep error code semantics clear, prefer using a more accurate code, such as:
missing_configmissing_env- or
invalid_input
At minimum, for commands like wallet creation/import—which do not require prior authentication but do require environment-provided credentials—avoid reusing the same auth_required semantics as “please login first”.
| if (mode == OutputMode.JSON) { | ||
| emitJsonError(current.errorCode, current.errorMessage); | ||
| } else { | ||
| out.println("Error: " + current.errorMessage); | ||
| if (current.usageHelp != null) { | ||
| out.println(); | ||
| out.println(current.usageHelp); |
There was a problem hiding this comment.
In text mode, error output is currently written to stdout instead of stderr, which goes against common CLI conventions.
Location
OutputFormatter.java(line 140)
Problem
In OutputFormatter.flush(), when an error occurs in text mode, it executes:
out.println("Error: " + current.errorMessage);instead of writing to err. This leads to several architectural issues:
- Callers cannot easily distinguish between normal output and error output
- In shell pipelines, redirection, and script integrations, error messages are mixed into
stdout - This contradicts the class design, which already maintains an
errstream info()already useserr(OutputFormatter.java line 221), indicating that a stdout/stderr separation model exists, buterror/usageErrorin text mode do not follow it
Tests have already baked in this behavior:
- Multiple
StandardCliRunnerTestcases explicitly assert that text-mode errors appear instdoutand thatstderris empty - Fixing this will require updating the test contract accordingly
Recommendation
- In text mode, route error and usage/help output to
err, and keepstdoutfor successful results only - In JSON mode, continuing to write everything to
stdoutis acceptable
This aligns better with standard CLI conventions and improves scriptability.
|
|
||
| @Test | ||
| public void changePasswordUsesGlobalWalletOverrideAsExplicitTarget() throws Exception { | ||
| File tempDir = Files.createTempDirectory("change-password-global-wallet").toFile(); |
There was a problem hiding this comment.
The usage of temporary directories in tests is widespread, but most cases lack corresponding cleanup and currently rely on the system’s temp directory reclamation.
Example Locations
StandardCliCommandRoutingTest.java(line 155)StandardCliRunnerTest.java(line 300)ActiveWalletConfigTest.java(line 55)
Problem
There are more than twenty usages of Files.createTempDirectory(...) across the repository, but explicit cleanup is only implemented in a few places, for example:
StandardCliRunnerTest.java(line 319)ActiveWalletConfigTest.java(line 85)ClearWalletUtilsTest.java(line 32)
Most other tests leave directories behind in the system temp path. While this usually does not affect correctness for a single run, it introduces several maintenance issues:
- Temp directories accumulate over time in local and CI environments
- It becomes harder to distinguish which leftover directories belong to the current test when debugging failures
- Inconsistent test style: some tests clean up manually, others do not
Recommendation
Adopt a consistent strategy:
- Use
@Rule TemporaryFolderor a unified temp helper; or - At minimum, add
deleteOnExit()orfinally-based cleanup to allcreateTempDirectoryusages
Avoid mixing “partial cleanup” and “no cleanup” approaches.
| json.put("private_key", priKeyHex); | ||
| out.success("Address: " + addressStr + "\nPrivate Key: " + priKeyHex, json); |
There was a problem hiding this comment.
generate-address returns the private key in plaintext in JSON mode. This behavior appears intentional, but there is currently no explicit security warning in the command description, making it easy for automated callers to underestimate the risk.
Locations
MiscCommands.java(line 34)MiscCommands.java(line 46)MiscCommands.java(line 49)
Problem
generate-address currently outputs results to both:
- text message:
Address: ... / Private Key: ... - JSON data:
{ "address": "...", "private_key": "..." }
This means that whenever a user enables --output json in scripts, the private key will consistently appear in structured stdout output.
This may not be a bug, since the command is designed to generate an offline address and return the private key. However, it is a highly sensitive interface and fundamentally different from normal query commands. The current description:
"Generate a new address offline"
does not indicate that the output includes a private key. Neither the help text nor command metadata provides any risk warning, making it easy for callers to treat it like a normal address-generation command and inadvertently pipe it into logs, pipelines, or audit systems.
Recommendation
- At minimum, explicitly warn in the command description or help that the output includes a plaintext private key
- For additional safety, consider adding a prominent warning in text mode; for JSON mode, document that stdout should not be logged
- Ideally, also document this as an explicit security constraint in the README / standard CLI contract
| Assert.assertTrue(json.contains("\"success\": true")); | ||
| Assert.assertTrue(json.contains("\"data\": {")); | ||
| Assert.assertTrue(json.contains("\"message\": \"hello\"")); |
There was a problem hiding this comment.
Many JSON-related tests currently use String.contains(...) to assert serialized output, with assertions written as space-sensitive fragments such as "\"success\": true" or "\"error\": \"usage_error\"". This ties the tests to the current pretty-print format rather than the JSON semantics.
Example Locations
OutputFormatterTest.java(line 21)TransactionCommandsTest.java(line 87)StandardCliCommandRoutingTest.java(line 456)
Problem
These assertions depend not only on field presence, but also implicitly on:
- Gson pretty-print retaining the
": "spacing style - Field names and formatting layout remaining unchanged
- JSON not being compacted or serialized by a different serializer
If the JSON output is later switched to compact mode, a different serializer, or even minor formatting adjustments, many tests will fail despite no semantic change.
From the test intent, they are trying to verify that certain fields and values exist in the JSON. A more robust approach is to parse the JSON first and assert structurally.
Recommendation
Refactor these assertions to:
JsonObject root = JsonParser.parseString(json).getAsJsonObject();Then assert structurally, e.g.:
root.get("success").getAsBoolean()root.get("error").getAsString()root.getAsJsonObject("data").get("txid")
Using contains(...) is still acceptable for checking free-form text messages, but it should not be used for core JSON contract validation.
| local args_string="$1" | ||
| local substituted | ||
| substituted="$(qa_substitute_placeholders "$args_string")" | ||
| python3 - <<'PY' "$substituted" |
There was a problem hiding this comment.
QA scripts heavily depend on python3, but the entry scripts do not perform a prerequisite availability check. When the environment is missing Python, failures occur mid-execution with obscure shell errors.
Locations
qa/run.sh(line 1)qa/task_runner.sh(line 1)qa/lib/cli.sh(line 244, line 275)qa/lib/semantic.sh(line 11)qa/lib/case_resolver.py(line 1)
Problem
There are multiple direct invocations of python3 and usages of #!/usr/bin/env python3 in the QA stack, but qa/run.sh / qa/task_runner.sh do not include a unified check such as:
command -v python3 >/dev/null 2>&1 || { echo "python3 is required"; exit 1; }As a result, in environments without a Python runtime, users do not receive a clear failure message at entry. Instead, they encounter errors like python3: command not found deep inside helper scripts during execution. For a QA toolchain entry point, this indicates insufficient upfront dependency validation.
Recommendation
Add a require_python3() check at the entry point in qa/run.sh, with a clear error message indicating:
python3is required- QA scripts depend on it for JSON / manifest / case parsing
For completeness, you may also check other dependencies such as java, bash, and gradle, but at minimum python3 should fail fast.
| run_task_file_parallel() { | ||
| local task_file="$1" | ||
| local jobs="$2" | ||
| [ -s "$task_file" ] || return 0 | ||
| xargs -P "$jobs" -n 2 bash "$SCRIPT_DIR/task_runner.sh" < <(tr '|' '\n' < "$task_file") | ||
| } |
There was a problem hiding this comment.
The QA system currently lacks per-test-case timeout control. If a single case hangs, the entire worker process can remain stuck indefinitely, and in serial phases this can block the whole run.
Locations
qa/run.sh(line 109, line 116)qa/task_runner.sh(line 98, line 103)qa/lib/cli.sh(line 403)
Problem
The current execution model is:
qa/run.shusesxargs -P ... bash task_runner.shto schedule cases in paralleltask_runner.shdirectly invokesqa_run_raw_capturefor each caseqa_run_raw_capturedirectly executes the underlying command
There is no timeout / watchdog / per-case deadline control anywhere in this chain.
As a result, if a command hangs due to network issues, node problems, leftover interactive state, or a stuck subprocess:
- The worker holds onto a concurrency slot indefinitely
- Serial phases can block the entire QA run
- Failures manifest as “the script never finishes” instead of a clear
TIMEOUT, making diagnosis difficult
Recommendation
Wrap each test case with a unified timeout, e.g., in task_runner.sh or qa_run_raw_capture:
timeout "${QA_CASE_TIMEOUT:-60s}" ...On timeout, write the result as FAIL: timeout.
- Make the timeout configurable via an environment variable
- Ensure hung cases do not stall the entire QA pipeline
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