Skip to content

Release v4.11.0 - #971

Merged
gummy789j merged 26 commits into
masterfrom
release_v4.11.0
Aug 5, 2026
Merged

Release v4.11.0#971
gummy789j merged 26 commits into
masterfrom
release_v4.11.0

Conversation

@gummy789j

Copy link
Copy Markdown
Collaborator

New Features

Change

  1. Multi-sig is now a first-class workflow in the TypeScript CLI. permission show and permission update read and replace an account's permission structure on chain (the latter burns 100 TRX and warns before an owner lockout); every broadcast command gained --permission-id <n>, --expiration <ms>, and a new --build-only early-exit mode alongside --dry-run / --sign-only. tx sign now takes a transaction hex via --hex / --file, appends exactly one signature while preserving prior ones, and reports how far the accumulated weight is from the threshold — refusing before a key is ever decrypted if the account is not in the permission group (not_authorized) or has already approved (already_signed); --offline keeps the air-gapped path. tx approvals is the read-only companion that shows the permission group, threshold, accumulated weight, approved signers, and expiration without signing. (#963)

  2. tx multisig adds optional collaboration through the TronLink multi-sig service. Where the on-chain path passes a hex from person to person, the service holds the transaction, accumulates signatures, and pushes notifications: the default mode lists transactions awaiting this account, --create signs an unsigned hex and opens a collection at 1 / N, --sign <txId> co-signs a pending one, and --watch keeps a WebSocket open and reports only the count awaiting your signature (never transaction content). Once the threshold is reached the service broadcasts. Credentials (tronlinkSecretId / tronlinkSecretKey / tronlinkChannel) are set with config and are per-environment; without them the command fails with tronlink_credentials_missing. (#963)

  3. gasfree brings gas-less token transfers to the TypeScript CLI. gasfree info reports your GasFree address, activation status, nonce, and fee schedule; gasfree transfer signs an EIP-712 structured-data transfer and submits it to the GasFree provider, which puts it on chain and charges the fee in the transferred token itself, so no TRX is required; gasfree trace <traceId> follows the provider's WAITING → INPROGRESS → CONFIRMING → SUCCEED / FAILED states. Because submission goes to a provider rather than a node, the receipt carries a traceId instead of a txId — follow it with --wait or gasfree trace, not tx status. Credentials are gasfreeApiKey / gasfreeApiSecret via config. (#963)

  4. New account-lifecycle and local-utility commands. account activate creates a not-yet-existing address on chain without transferring any asset, with the payer covering the creation fee; account set writes the on-chain name or account id (permanent on mainnet, so it is deliberately one-at-a-time). A local contact book — contact add / list / remove, stored 0600 in the config directory — lets a saved name be used wherever a recipient is expected (tx send --to, gasfree transfer --to). encoding convert <input> auto-detects an input and prints every equivalent representation (TRON base58 / TRON hex / EVM / public key, or hex / Base64 / Base58Check), rejecting secrets outright. address generate produces a throwaway keypair offline, writing the private key to a 0600 file rather than the terminal unless --print-secret is passed. current --qr renders a scannable receive-address QR in text mode without unlocking or touching the network. (#963)

  5. The Java documentation was split out of java/README.md into a browsable java/docs/ tree. The README shrank from a single ~2,700-line page to an overview with quick links; commands now live under docs/commands/ by domain (wallet, account, transfers, staking, multisig, GasFree, DEX, proposals, contracts, …), with docs/concepts/, docs/guide/, and a field-by-field docs/reference/config.md. Stale planning and QA scratch documents were removed. No Java behavior changed in this release apart from the version constant. (#963)

Bug Fixes

Change

  1. A transaction the node rejected could be reported as submitted. TronWeb does not throw on rejection — it hands back the node's response verbatim, and a rejected /wallet/broadcasttransaction carries no result field at all, so the previous result === false check never fired and the CLI returned a success envelope with a txId for a transaction that was never accepted. Acceptance is now white-listed (result !== true is a rejection) on both the object and --hex broadcast paths, surfacing the node's reason as transaction_rejected. (#963)

  2. block lost precision on large int64 fields. Block responses were parsed through JavaScript numbers, so protobuf int64/uint64 values beyond Number.MAX_SAFE_INTEGER came back rounded. Blocks are now fetched and parsed losslessly, with out-of-range integers preserved as exact decimal strings. (#963)

  3. --dry-run reported doomed staking requests as fine. stake freeze and stake unfreeze are rejected by the node at broadcast time when the amount exceeds the available balance or the amount actually staked for that resource, but --dry-run never reaches the node — so a request that could not possibly succeed previewed cleanly. Both now pre-flight against the chain and fail with insufficient_balance / insufficient_stake. Relatedly, contract send now warns when the supplied --fee-limit is below the energy estimate at the current energy price, and the staked-amount computation was corrected for the node's habit of omitting the default BANDWIDTH enum in frozenV2. (#963)

  4. Chain-controlled text could repaint the terminal. Permission names, token names and symbols, and co-signer labels are written by whoever put them on chain. Terminal control bytes were already stripped, but bidirectional and zero-width formatting characters passed straight through — U+202E reverses the display order of everything after it, letting a crafted name change how the address or weight beside it appears, and zero-width characters can make two different names render identically. Text mode now escapes them visibly (<U+202E>) rather than dropping them, so legitimate right-to-left text still displays normally and tampering is obvious. JSON output is never rewritten. (#963)

  5. Filesystem failures surfaced as raw OS errors. backup and the atomic config/keystore writers let Node ErrnoExceptions escape, producing internal_error and, on a failed write, leaving a truncated or empty file at the destination. Failures are now typed as io_error with the partial artifact removed, and the multi-file atomic write reports a rolled-back failure distinctly from one that committed but could not confirm durability. Two new config-file conditions are also reported explicitly: invalid_config when config.yaml cannot be parsed (the parser detail is withheld, since it quotes the offending line and may carry a credential) and insecure_config when the file holds service credentials but is a symlink or group/world-readable. (#963)

  6. Positional arguments rejected number-shaped values. Yargs inferred a type for the anonymous positional tail, so a value such as 0xdeadbeef or 12345 arrived at its string-typed field as a number and was rejected. The tail is now typed as strings so the raw token survives parsing. As a consequence, a field exposed as a positional no longer also accepts its --<field> spelling — this affects the undocumented config --key/--value and block --number forms; the documented positional form is unchanged, and the global --account flag still works on use / rename / delete / backup. (#963)

gummy789j and others added 26 commits August 5, 2026 12:14
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
Positional arguments: yargs inferred a type for the anonymous group tail, so a
number-shaped value (0xdeadbeef, 12345) reached its string-typed field as a
number and was rejected — `encoding convert 0x...` could not take the EVM
address its own help example shows. Type the tail as strings so the raw token
survives parsing (coercing afterwards is lossy: 1e5 would already be 100000),
and route single-segment leaves through the same `args..` capture so one
binding path serves every positional command.

tx sign: replace --check with --offline and verify online by default. A
co-signer outside the transaction's permission group, or one signing twice, now
fails before any signature is produced, instead of emitting a hex that only
`tx broadcast` rejects — possibly after it has been passed to further signers.
The default receipt carries the documented permission and weight-progress
block; --offline keeps air-gapped signing and says plainly that approval state
was not checked. The 4.10.0 --transaction JSON route is untouched.

account activate --dry-run: render the derived account-creation fee as its
total rather than "[object Object]", and stop an unrecognised fee object from
reaching the scalar fallback that stringified it.

BREAKING CHANGE: a field exposed as a positional no longer accepts its
`--<field>` spelling. This affects `config --key/--value`, `block --number`,
`contact add --name/--address`, `contact remove --name`, `encoding convert
--input` and `gasfree trace --trace-id`. The positional form is unchanged, and
the global --account flag still works on `use`/`rename`/`delete`/`backup`.
…round trip

Text receipts carried machine-readable values that belong in json: the
contract-type enum sat next to the human operation name on every Type row, and
`permission show` printed the raw operations bitmap. Both are now json-only;
the enum remains the fallback when a contract type has no human name.

`tx broadcast` resolves the full approval state to decide broadcastability but
projected none of it into text: --dry-run printed a header and a fee line while
json carried permission, weight progress and approved signers. Reuse
renderApproval for it, which also exposed two smaller faults — the multi-sign
fee was printed twice whenever it was non-zero, and the Tx row was always empty
because the broadcast view names the field `transaction`, not `tx`. renderApproval
moves to its own module so tx.ts can use it without a cycle back into multisig.ts.

`permission update` rejected the workflow its own documentation recommends.
`permission show` always emits unknownOperationIds, almost always `[]`, and an
empty list forced operationsHex to be supplied alongside it — so a structure
exported by show could not be fed back after editing `operations`. An empty list
declares "no unnamed bits", exactly what re-encoding `operations` produces, so it
now needs no bitmap; a non-empty list still does, leaving the review guarantee
intact. The remaining mismatch error now names the fix instead of only stating
the disagreement.

Eight fields across four commands shipped with no help description, five of them
positionals rendered as bare names under Args. All are filled in, and a
registry-wide test now fails if a command ships an undocumented input.
Resolves the 15 findings raised on the PR. Verified each against the code
before changing anything; several turned out to differ from the report.

Correctness
- contract deploy reported an address derived from the pre-prepare txID.
  --permission-id / --expiration rewrite raw_data, moving the txID and with
  it the deployed address. Derived in refreshTransactionIdentity now, and
  captured from the prepared transaction. The default path was never wrong.
- Post-checks that run after an already-confirmed, already-paid transaction
  no longer convert it into a command failure (account activate / set,
  permission update). New warnOnPostCheck degrades both the unreadable-read
  and the mismatch paths to warnings, keeping the txid.
- GasFree polling no longer discards the accepted receipt: transport
  flakiness retries inside the deadline, other non-integrity errors return
  submitted with the traceId, and integrity errors now carry it in details.
- getTokenInfo no longer swallows transport failures. Measured against a
  live node: a view method the contract lacks returns result:true with an
  empty payload, so the catch was only ever hiding node outages, which
  GasFree then escalated to gasfree_integrity and tx send to exit 2.
- Keypair writer removes its own unfinished file, so a failed write no
  longer blocks every retry of the same path via O_EXCL.
- TronLink signer rosters are bound to the on-chain permission, not just
  the signed subset; weights render from chain. A stale snapshot after a
  permission update was displayed as chain-validated.

Contract and output
- Bootstrap failures produce a v1 error envelope and the right exit code
  instead of a bare fatal: line; config read/parse failures classify as
  invalid_config without echoing file content.
- Exclusive-output conflicts use one code, class and exit status
  (output_exists / UsageError / 2) across backup and address generate.
- Text output escapes bidi and zero-width characters rather than passing
  them through, so a chain-controlled name cannot reorder what is printed
  beside it. JSON stays byte-exact.
- Shielded transfers are named in the codec table so the refusal is
  actionable; four contract types cannot be decoded, not the three the
  docs claimed.

Architecture
- TransactionArtifactWriter port added; the inbound command no longer
  names the outbound implementation. Pinned by a boundary test, since
  depcruise cannot see type-only edges without failing on pre-existing
  type-only cycles.

Docs
- meta.warnings declared as the mixed type it actually is, with guidance
  for consumers.
- --wait receipts documented as reporting the outcome in data.stage, never
  in success.
- The 11 leaf pages that had fallen behind the shared transaction options
  now document --build-only / --permission-id / --expiration, and no
  longer claim every mode needs the master password. Guarded by a test.
- Corrected claims that were never implemented (vote status warning code),
  no longer reachable (account set provider_error), or absent (config file
  mode requirement and its Windows exemption).
…t build

depcruise only saw the graph that survives compilation, so a boundary
violation carrying nothing but a type was invisible to it — which is how
an inbound command came to name an outbound implementation directly.
Turning on tsPreCompilationDeps fixes that, but it also closes four
pre-existing type-only cycles, so those had to go first.

Every cycle ran through one pivot: ChainFamily was defined in the family
registry, and the registry depends on the address codec at runtime. Any
type that named a family therefore had to reach through the registry,
closing types -> family -> address -> types. The identity now lives in its
own dependency-free module, re-exported from the registry, so both public
import paths are unchanged.

The price cycle was simpler: coingecko imported its own port type back
through the barrel that re-exports it, rather than from the port.

With the cycles gone, tsPreCompilationDeps is on and all four boundary
rules cover type-only edges. That subsumes boundary.test.ts, added as a
stopgap for the inbound->outbound case, so it is removed: the guarantee
moves from one hand-written string check to the build, and from one edge
to four.

Verified the replacement is real by pointing the inbound command back at
the outbound class: depcruise reports inbound-does-not-know-outbound.
--create was ported from MultiSignService.createTransaction(), which has no
call site anywhere in the Java tree — dead code whose request shape had never
been validated by the service. Two things were wrong as a result.

The POST signature covered permission_name and tx_id, but the service signs
only sign_version/channel/secret_id/ts/uuid/address, so every --create was
rejected with 4000 Authentication failed. And the shape itself does not
exist: the service has no empty collection, and derives the starting weight
from the signature the transaction arrives with. Uploading unsigned raw_data
returns 20004 Error param.

--create now signs the transaction locally and submits it, which opens the
collection at 1 of N. The CLI surface is unchanged — it still takes the
unsigned hex a --build-only run produces — but it needs the master password
now, and the originator no longer signs a second time. signChecked() already
covers expiry, permission membership, and repeat signatures, so the manual
getSignWeight/approvals preamble is gone, along with the port's create() and
TronLinkCreateRequest.

Two related fixes:

- Service rejections carried only a numeric code, which cannot separate a bad
  signature (4000) from a bad parameter (20004) or a stale one (20305). The
  service's own wording now reaches error.details.providerMessage, stripped of
  control characters and bounded at 200 chars.

- One record that no longer reconciles with the chain used to fail the whole
  listing, which is the normal state of any account whose permission changed
  while an old transaction sat queued. Listing now marks such a record
  unverified, says why, and shows the rest of the page. It is never presented
  as chain-validated, its state column reads unverified rather than what the
  service claimed, and awaitingMySignature is forced false. Acting on one still
  refuses. "Could not check" is kept distinct from "checked and disagreed":
  a node outage propagates instead of reading as a clean page.

Also: once the threshold is met the service broadcasts the transaction itself,
so the receipt now says to confirm with tx info before broadcasting by hand.

Verified end to end on Nile: --create -> --sign -> on chain
(a480c8f6…5797, block #69,754,801). New tests are mutation-checked; the ten
mutants covering the routing, receipts, degradation boundary, and message
handling all fail the suite.

Bundles an unrelated in-flight refactor: KeypairWriter now takes a
{out}|{name} target so filesystem layout stays out of AddressService.
The v0.1.2 help audit found that a command's --help routinely omitted what
the command actually requires, so a reader could only learn the rule by
failing a run. Fix that at the source: declare the constraints, render them,
and cover each with a test.

Mutually exclusive options (A-1)
  Options that are jointly required each rendered "[optional]" — individually
  true, but read together it says the whole set may be omitted, which is
  exactly what Zod rejects. Add a declarative ExclusiveGroup on the spec,
  rendered as a labelled block and emitted in the --json-schema catalog so
  agents see it too. Two flavours: "exactly-one" drops the misleading tag,
  "at-most-one" keeps it (tx send's asset selector really is optional — omit
  all three and you send native TRX). Eight groups, three of which the audit
  did not list: tx send x2 and message sign, whose --message-stdin never
  appeared under Options at all.

Values and limits (A-2, A-3, A-4, A-5)
  --permission-id now names its groups (0=owner, 1=witness, 2-9=active).
  --expiration states the 24h cap it enforces and the ~60s node default when
  omitted. --build-only names both multi-sig routes it feeds, not just the
  TronLink one. address generate --out names the default path a private key
  lands on, which mattered before the run, not only in the receipt.

Irreversible and costly operations (A-6, A-8, A-9, A-11)
  account set says each field can be set once and never changed, and is not
  `rename`. permission update says where the input shape comes from and that
  the lockout warning does not block the submission. The permission group page
  explains the model and names the update fee; the gasfree group page says
  fees come out of the token being sent.

Wording that was false (B, root)
  --wait claimed "after broadcast ... returns the submitted txid", shown
  verbatim on gasfree transfer, which broadcasts nothing and returns a trace
  id. Reword globally rather than add a per-command override — a second copy
  is how this drift started. --timeout enumerated only RPC and device, leaving
  out the service APIs it equally bounds. chain was the one family-scoped
  group missing its (tron) tag.

Also: permission update carried its own copies of three shared txModeFields
descriptions; they now reuse the shared source, and a test keeps them there.
Fourteen reference pages get the same --permission-id / --expiration wording,
with the sync test tightened from "flag is present" to "semantics match",
reading the expected text out of the schema instead of restating it.

Ledger reach (separate thread, folded in)
  backup checks exportability before demanding a master password: a
  Ledger-only keystore may have no password sentinel, so the old order trapped
  the user on a prompt no answer satisfies. account activate and account set
  --id now require a software signer up front — the TRON app has no parser for
  AccountCreateContract or SetAccountIdContract and answers 0x6a80, which is
  now classified with wording covering both an unsupported contract type and a
  malformed payload.

Tests 792 -> 833. Two defects the tests caught that review would not: an
exclusive group whose member was named in camelCase resolved to nothing and
vanished silently (the renderer now throws), and each shared-description edit
left a stale duplicate behind in permission update.
TronLink renders `resource` as its enum value (`1`), while TronWeb's encoder
only understands the name and drops an unrecognized value silently. The
re-encoded bytes then disagreed with the provider's own raw_data_hex, so every
freeze / delegate record read as forged. Against Nile, 7 of one account's 20
records failed; because the failure was thrown from the per-record projection,
one such historical record hid the entire queue and `tx multisig` exited 1.

Map the value back to its name before encoding. The raw_data_hex / txID
equality checks remain the arbiter of whether the result is accepted, so this
widens what can be read, not what can be trusted.

Two conditions around it, kept distinct: a record this client cannot
reconstruct now costs its own row (counted in `unreadable`, reported below the
table) rather than the page, while a record that decodes and then disagrees
still fails the command as before. The error names the codec's reason instead
of blaming the service for bytes we could not rebuild.

Verified against the reported account on Nile: 7 failures -> 0.
…e node

`permission update` submits through the shared pipeline, whose preflight only
checked that the signer belongs to the transaction's permission group and has
not already signed. Nothing compared the accumulated signature weight against
the permission threshold, so an owner holding one key of a 2-of-2 group signed
locally and broadcast a transaction the node was always going to reject —
while the same transaction handed to `tx broadcast` failed locally with
`not_authorized`, never touching the network.

Preflight now returns a `SignerAuthorization` gate that the pipeline consults
after signing and before broadcasting, in broadcast mode only: sign-only and
build-only still return a partial signature, which is how a multi-signature
transaction legitimately reaches its threshold. The weight is projected from
data the preflight already fetched, so the check costs no extra RPC, and
commands that pass no preflight — ordinary transfers — are untouched.

Both entry points now raise the shortfall through one `assertThresholdReached`,
so their error code and message are identical by construction.

Closes PER-005 / DEFECT-005. Validated on Nile: an under-threshold
`permission update --broadcast` is refused three times out of three with no
txid and no fee burned, and the same update lands once both owners sign.
@gummy789j
gummy789j merged commit 33a071b into master Aug 5, 2026
1 check passed
@jj5419952-stack

jj5419952-stack commented Aug 5, 2026 via email

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants