Feat/ts v4.11.0 - #963
Conversation
Feat/ts signing commands
Feat/ts v0.3.0
Backfill reference pages for every command added since release_v4.11.0, verified against the command registry rather than the drafted command doc: all 66 registered commands now have a page. New pages: permission/ (index, show, update) multi-sig permission structure gasfree/ (index, info, transfer, trace) contact/ (index, add, list, remove) address/ (index, generate) encoding/ (index, convert) tx/approvals, tx/multisig account/activate, account/set Updated: commands/index, account/index, tx/index (multi-sig lifecycle), tx/broadcast (--hex/--file/--dry-run, threshold validation), tx/send (contact names), current (--qr), config (TronLink/GasFree credentials and secret masking), README, and the agent SKILL command map. Examples and error codes were taken from actual command output and the domain rules, so contact limits, the permission operation bitmaps, and the config masking behaviour match the implementation. Also included in this commit: - bump version to 4.11.0 (ts package, runner, java Utils) - name TRON contract types 3/20/32/51 and require unnamed operation bits to be declared via unknownOperationIds - add USDD to mainnet and seed the Nile builtin token book
zerodevblock-cyber
left a comment
There was a problem hiding this comment.
Review summary
I validated the 15 selected findings against PR #963 at commit 1e068529da4bf2535618acb228fa5597da5c49de. Each finding was rechecked against the aggregate diff, its source-to-sink call path, existing guards, tests, and a safe local or in-memory reproduction where feasible. The detailed findings are attached as inline comments.
Findings
- P1 (2)
CR-020: transaction preparation can change the deployment txID while the CLI keeps reporting the address derived from the pre-prepare txID.CR-016: documented service-secret configuration passes the expanded credential through OS argv before output masking applies.
- P2 (5)
CR-002: post-confirmation read failures overwrite an irreversible success result.CR-003: a polling error after GasFree acceptance discards the trace ID and submitted receipt.CR-007:wallet-cli.result.v1changes warning elements from strings to objects without versioning.CR-008: the transaction codec rejects protocol type 51 despite the published support surface.CR-021: insecure-config bootstrap failures bypass the JSON envelope and UsageError exit code.
- P3 (8)
CR-005,CR-009,CR-010,CR-012,CR-013,CR-014,CR-015, andCR-022cover file-write atomicity, dependency direction, error classification, documentation drift, machine-error consistency, GasFree failure semantics, terminal bidi safety, and TronLink signer metadata validation.
The highest-priority fixes are to bind the reported deployment address to the final prepared transaction (CR-020) and move service secrets off argv (CR-016). The P2 items should also be resolved before release because they affect irreversible-operation recovery, machine compatibility, supported transaction types, and deterministic CLI error handling.
Validation checks completed:
- the PR head and reviewed HEAD are identical;
- all 15 inline anchors are added lines in the current aggregate PR diff;
- the relevant targeted suite passed (6 files, 50 tests);
npm run typecheckpassed;- no external service was called, no real credential was used, and no transaction was signed or broadcast.
| account: scope.activeAccount, | ||
| broadcaster: gateway, | ||
| ...transactionMode(input), | ||
| ...tronTransactionHooks(gateway), |
There was a problem hiding this comment.
[P1][CR-020] Recompute the deployment address after transaction preparation
tronTransactionHooks() runs after build(). A non-zero permission ID or a custom expiration changes raw_data and recomputes the txID, but contractAddress is still the value captured from the builder transaction. TronWeb derives the contract address from the owner and txID, so the CLI can report a different address from the contract that is actually deployed.
Please derive the address from the final prepared transaction, or have the pipeline return the final derived address. A regression test should cover non-zero permission IDs and custom expirations.
| Configure a service credential — note that the value never appears in the output: | ||
|
|
||
| ```bash | ||
| wallet-cli config gasfreeApiSecret "$SECRET" -o json |
There was a problem hiding this comment.
[P1][CR-016] Do not pass service credentials through process arguments
This example expands $SECRET before the process starts, so the credential is present in OS argv even though the command masks its output. Process inspection, endpoint telemetry, CI tracing, or command logging can capture it; this also contradicts the machine-interface promise that secrets never travel via argv.
Please provide a dedicated stdin, hidden TTY, or file-descriptor secret input and update this example to use that channel.
| estimate: async () => ({ ...fee, balanceSun }), | ||
| }); | ||
| if (outcome.stage === "confirmed" && !outcome.failed) { | ||
| const activated = await gateway.getAccount(input.address); |
There was a problem hiding this comment.
[P2][CR-002] Preserve the confirmed receipt when this post-check cannot be read
At this point the transaction has already returned confirmed. If this follow-up RPC times out, is rate-limited, or observes a lagging node, the exception escapes the use case and replaces the confirmed txid with a command failure. Callers can then retry an operation that already succeeded.
Please retain the confirmed/submitted outcome and report an observation warning for read failures. The account-set and permission-update post-checks have the same partial-success boundary and should follow the same rule.
| const remaining = deadline - this.clock(); | ||
| if (remaining <= 0) return undefined; | ||
| await this.sleep(Math.min(POLL_MS, remaining)); | ||
| const current = await this.provider.trace(network, initial.id); |
There was a problem hiding this comment.
[P2][CR-003] Preserve the accepted GasFree receipt when polling fails
submitTransfer() has already succeeded and the trace ID has been captured before this request runs. A transient timeout, 429, or provider 5xx escapes from here, causing the entire command to fail without returning that trace ID. Only normal deadline exhaustion currently preserves the submitted receipt.
Please retry retryable polling failures within the wait deadline, or return the existing accepted result with a warning so callers can reconcile the original submission instead of resubmitting it.
| export interface Meta { | ||
| durationMs: number; | ||
| warnings: string[]; | ||
| warnings: WarningItem[]; |
There was a problem hiding this comment.
[P2][CR-007] Keep the v1 warning element type stable
The envelope still declares wallet-cli.result.v1, while the published machine contract defines meta.warnings as string[]. Allowing WarningView here serializes {code, message} objects for permission warnings and can break existing v1 consumers that parse, join, or display every element as a string.
Please keep warnings as strings in v1, or introduce the structured form through a versioned field/schema with an explicit migration path.
| signOnly: z.boolean().default(false).describe("sign and output the transaction without broadcasting; mutually exclusive with --dry-run; broadcast later with tx broadcast"), | ||
| dryRun: z.boolean().default(false).describe("build and estimate only, with no signature and no broadcast"), | ||
| signOnly: z.boolean().default(false).describe("sign and output complete transaction hex without broadcasting"), | ||
| buildOnly: z.boolean().default(false).describe("build and output unsigned complete transaction hex without unlocking"), |
There was a problem hiding this comment.
[P3][CR-012] Update every leaf command page for the new transaction modes
--build-only, --permission-id, and --expiration are now shared by 11 transaction commands, but their individual reference pages still document only the previous dry-run/sign-only surface; some also still imply that every non-dry-run mode unlocks the wallet.
Please update the affected leaf pages and consider validating each page's option set against the command registry in CI so shared schema changes cannot leave command documentation behind.
| const code = (error as NodeJS.ErrnoException).code; | ||
| if (code === "EEXIST" || code === "ELOOP") { | ||
| throw new ExecutionError( | ||
| "file_exists", |
There was a problem hiding this comment.
[P3][CR-013] Reuse the existing output-conflict error contract
An existing target is reported here as file_exists via ExecutionError (exit 1), while the existing backup writer reports the equivalent O_EXCL conflict as output_exists via UsageError (exit 2). Automation can therefore treat the same deterministic conflict as retryable in one command and malformed input in another.
Please use one stable code, error class, and exit-code policy for exclusive-output conflicts, with a compatibility plan if the older code must remain supported.
| terminal.txnActivateFee ?? accepted.activateFee; | ||
| return { | ||
| ...accepted, | ||
| stage: terminal.state === "SUCCEED" ? "confirmed" : "failed", |
There was a problem hiding this comment.
[P3][CR-014] Do not expose a FAILED transfer as top-level success
This branch resolves normally with stage: "failed", so the shared shell formats it as success: true and exits 0. The published machine interface tells automation to branch on exit code/top-level success first and does not define a GasFree-specific secondary check, so a failed transfer can be recorded as successful.
Please either return a failure envelope/non-zero exit for the FAILED terminal state, or explicitly version and document a stable two-level status contract. Add an end-to-end assertion for exit code, top-level success, stage, state, and failure reason.
| function permissionCard(permission: PermissionGroupView, active = false): string { | ||
| const activePermission = active ? permission as AccountPermissionsView["actives"][number] : undefined; | ||
| const lines = [ | ||
| `Permission Name ${permission.name} (id ${permission.id}${active ? ", active" : ""})`, |
There was a problem hiding this comment.
[P3][CR-015] Escape Unicode bidi controls in untrusted display fields
Permission names are chain-controlled and reach this security-sensitive terminal summary. The shared text sanitizer removes ANSI/C0/C1 bytes but leaves Unicode bidi controls such as U+202E and U+2066–U+2069, allowing the visible order of names, IDs, or adjacent labels to differ from the underlying string. GasFree and multisig renderers expose the same class of input.
Please escape or visibly mark bidi/default-ignorable controls in text mode while preserving the original value in JSON, and add terminal golden tests for the affected renderers.
| || approval.signatures !== remote.view.signatures | ||
| || approval.expiration !== remote.view.expiration | ||
| || approved.size !== signedProgress.length | ||
| || signedProgress.some((address) => !approved.has(address)) |
There was a problem hiding this comment.
[P3][CR-022] Validate every progress entry against the on-chain permission
This comparison only binds the signed-address set to the chain-approved set. Unsigned progress entries and all per-signer weights remain provider-controlled, so TronLink can add the current account as an unsigned signer or redistribute individual weights while preserving the signed set and aggregate currentWeight. The list can then report a false awaitingMySignature state or incorrect weights.
Please compare every progress address and weight with the selected permission's on-chain keys and render chain-derived weights. signChecked() still prevents unauthorized signing, but the collaboration list should not present unverified signer metadata as chain-validated.
Summary
Brings the TypeScript CLI to the 4.11.0 release line: TRON multi-signature support end to end, gas-free token transfers, and local utilities (contacts, keypair generation, encoding conversion, account lifecycle, receive QR).
Command surface grows 52 → 66, and all 66 now have a reference page.
Scope is
ts/only, apart from a one-line version constant in Java.Base:
release_v4.11.0· 15 commits · 159 files, +12,991 / −216Multi-signature
The CLI can now participate in a multi-sig account for the whole lifecycle: configure permissions, collect signatures from several keys, broadcast once the threshold is met.
permission show/updatetx sign --hex/--file--checkverifies online)tx approvalstx multisig--create/--sign/--watch)tx broadcast --hex/--fileSignatures travel either offline as a hex artifact passed between signers, or through the TronLink service as a shared queue.
Safety properties worth review:
tx signon an artifact is offline by default; permission membership is only claimed with--check.tx broadcastrefuses a transaction below threshold (not_authorized) or expired (tx_expired) — itpermission updaterejects rather than normalizes (invalid_permission), emits lockout warnings, and re-reads after confirmation. A bitmap cannot silently widen a permission: unnamed set bits must be declared inunknownOperationIds.provider_error).--watchreceives a count only, never content.GasFree
gasfree info/transfer/trace— move USDT and other supported tokens with no TRX, by signing a TIP-712PermitTransferand letting the provider broadcast for a fee in the transferred token.Balance and fees are checked up front; the signed digest is recomputed and the recovered signer verified against the selected account (
signing_rejected); provider metadata is validated (gasfree_integrity).--dry-runprices without unlocking,--waitreports settled rather than estimated amounts. Mainnet and Nile only; Shasta returnsLocal utilities
contact add/list/removetx send --toand `gasfree transfer --toaddress generateencoding convert41…hex ⇄0xEVM, and hex / Base64 / Base58Checkaccount activate/setcurrent --qr0600); names resembling an to` can't be reinterpreted.address generatewrites the key to an exclusively created0600file and never overwrites; stdout requires explicit--print-secret.encoding convertrefuses any 32-byte input — indistinguishable from a private key, and argv is visiblcurrent --qrencodes exactly the address and never draws a partial QR (a truncated code could scan as a different address).Architecture & config
New ports keep hexagonal boundaries intact:
contact-repository,gasfree-provider,keypair-writer,qr-encoder,tronlink-collaboration. New pure-domain modules:contact,encoding,gasfree,permission.Five new optional config keys —
tronlinkSecretId/SecretKey/ChannelandgasfreeApiKey/ApiSecret. Both secrets are masked on read and write (********, including the echoedinput), so a config write is safe to log;unset keys are omitted entirely.
Documentation
19 new pages, 9 updated, following the existing layout. New groups
permission/,gasfree/,contact/,new pagestx/approvals,tx/multisig,account/activate,account/set; updatedtx/index(multi-siglifecycle),tx/broadcast,tx/send,current,config,README, and the agentSKILL.md.Written against the command registry and domain rules with examples taken from real output. Verified mechanically: all 66 commands have a page, and every relative link and anchor resolves.
Version
0.2.0→4.11.0for thetspackage, aligning it with the Java release line. JavaUtils.VERSIONmovesv4.10.0→v4.11.0.Verification
41 test files added or updated.
Behaviour changes
tx send --toaccepts a contact name; resolution is surfaced in the receipt anddata.toContact.tx sendauth is nowconditional—--build-onlybuilds unsigned without unlocking.tx broadcastvalidates before submitting, so some transactions now fail locally instead of on-chain.currenthonours--account, sodata.activeis no longer alwaystrue.--build-only,--permission-id,--expiration.