Skip to content

feat(ts): implement TypeScript wallet CLI#941

Open
gummy789j wants to merge 16 commits into
release_v4.9.7from
feat/ts-version
Open

feat(ts): implement TypeScript wallet CLI#941
gummy789j wants to merge 16 commits into
release_v4.9.7from
feat/ts-version

Conversation

@gummy789j

Copy link
Copy Markdown
Collaborator

Summary

Introduce a TypeScript implementation of the TRON wallet CLI with a hexagonal architecture, secure local wallet management, interactive TTY workflows, and software/Ledger signing.

Highlights

  • Add TRON account, wallet, token, transaction, staking, contract, block, network, and configuration commands.
  • Support TRON Mainnet, Nile, and Shasta.
  • Add encrypted local keystore and secure stdin/TTY secret handling.
  • Add interactive wallet creation, import, backup, deletion, and Ledger account selection.
  • Support software and Ledger transaction signing.
  • Add transaction dry-run, confirmation waiting, and structured transaction output.
  • Provide stable wallet-cli.result.v1 JSON envelopes and deterministic exit codes.
  • Redact addresses belonging to disabled chain families.
  • Organize the implementation using domain, application, inbound adapter, outbound adapter, and bootstrap layers.
  • Enforce architectural boundaries with dependency-cruiser.
  • Add unit, integration, golden-output, help-parity, and Nile live-test tooling.
  • Add architecture and development documentation.
  • Use npm for dependency and package management.

Architecture

The implementation follows a hexagonal architecture:

  • domain: wallet and TRON value objects and domain rules
  • application: use cases, services, contracts, and ports
  • adapters/inbound: CLI parsing, prompting, rendering, and output
  • adapters/outbound: TRON RPC, keystore, Ledger, persistence, pricing, and configuration
  • bootstrap: dependency composition and family registration

TRON-specific behavior is registered through family definitions without introducing a universal-chain abstraction.

CLI Guarantees

  • Stable wallet-cli.result.v1 JSON output
  • Deterministic 0, 1, and 2 exit codes
  • Exactly one terminal stdout frame in JSON mode
  • No secrets passed through argv or environment variables
  • Encrypted wallet storage
  • Software and Ledger signing support

gummy789j and others added 14 commits July 1, 2026 16:13
Add nativeSymbol/nativeDecimals to FamilyMeta and route account/token
balance rendering through a shared humanBalance helper. Refresh help
text, golden tests, and related command wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A hostile wallet label or remote token/RPC metadata value could carry
ANSI/OSC escape bytes that spoof or corrupt terminal display. Sanitize
C0/C1/DEL control bytes at the text-output boundary (success/error/event
frames); JSON mode stays byte-exact. (CLI-OUT-001)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…them

TronWeb's triggerSmartContract defaults to having the node assemble the
unsigned tx, letting a malicious/compromised RPC return a different
contract/recipient/amount than requested. Switch TRC20 transfer and
generic contract calls to txLocal so the calldata is ABI-encoded
client-side; every build path now assembles raw_data locally (the node
only supplies the ref block and broadcasts). Add a fail-closed tripwire
(assertBuiltTx) that rejects a tx whose contract count/type drifts from
the request, so a future re-route back through the node fails closed
instead of signing silently. (TRON-RPC-TX-SUBST-001/002/003)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump tronweb to ^6.4.0 and add overrides for axios (^1.18.1), ws
(^8.18.3), and esbuild (^0.28.1) to pull patched versions through the
tronweb, ethers, and Speculos transport paths. npm audit now reports 0
vulnerabilities (was 6: 4 high, 1 moderate, 1 low). (DEP-001)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address boundary-audit items with client-side guards, best-effort
degradation, and consistent error classification:

- big-integer precision: chain base-unit amounts (feeLimit, callValue,
  lockPeriod, block number) carry as strings and reject values past
  MAX_SAFE_INTEGER at the #safeNumber chokepoint, before TronWeb.
- contract deploy: drop the dead --constructor-sig flag; --params are raw
  positional constructor values (types come from the ABI). call/send keep
  {type,value} and now validate that shape at the command layer.
- tx info: transaction is the source of truth; a missing/failed info
  degrades instead of sinking the command (mirrors getContractMetadata).
- buildNativeTransfer: wrap the sendTrx build so node failures surface as
  rpc_error, not a redacted internal_error, matching sibling builders.
- --wait: a confirmation that never lands now records a meta.warnings
  entry instead of silently returning submitted.
- amounts: send/stake amounts must be positive (reject zero); contract
  call-value stays non-negative.
- account portfolio: per-token best-effort — one unreadable token
  degrades to a balanceError row without failing the whole portfolio.

Adds golden error-contract cases, a hermetic tron-confirmation unit test,
a per-token portfolio test, and a Nile contract-deploy test (live tiers
gated behind RUN_LIVE / RUN_LIVE_BROADCAST).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously --timeout only applied to write commands via TxPipeline; read
commands used a hardcoded 30s (TRON getAccount) or no bound at all (account
history, CoinGecko price). Hung reads could hang the CLI indefinitely.

Timeout work:
- Extract withTimeout into src/domain/async and reuse it across layers.
- TronRpcClient: take timeoutMs; wrap every #wrap'd read/build and broadcast,
  and drive the getAccount fetch AbortSignal off timeoutMs (was 30s). Timeout
  surfaces as ChainError("timeout"), not a remapped rpc_error.
- TronGridHistoryReader + CoinGeckoPriceProvider: take timeoutMs and pass an
  AbortSignal.timeout to fetch (history had no timeout before).
- Thread the effective timeout (globals.timeoutMs ?? config.timeoutMs) through
  composition -> gateway registry/factory, family deps, and price provider.
- TxPipeline: drop the now-redundant per-step withTimeout on build/estimate/
  broadcast/software-sign (adapters self-bound); keep only the device-sign
  bound + abort, which no RPC timeout covers.
- Reject a non-positive --timeout at the boundary: per-flag `min` (timeout=1)
  in the globals coercion, and config set timeoutMs must be positive. So 0ms
  (instant-abort) never reaches withTimeout/AbortSignal.

Tests: new adapter timeout tests (tron, history, coingecko), a pipeline
device-sign timeout+abort pin test, and config-service validation tests.

Also folds in in-progress parallel work present in the tree: price-provider
apiKey removal + per-contract CoinGecko pricing, keystore changes, wallet
import-watch promptHints + shell default-label text, wallet-service no longer
auto-activating a newly derived account, and doc updates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/not_found)

getTransactionInfo alone can't tell "unconfirmed" from "never existed" — it
returns {} for both. Pair it with getTransactionById (parallel) so a broadcast
tx reads as pending and an unknown hash reads as not_found instead of an
ambiguous "unknown". The renderer switches on the new `state`; `confirmed`/
`failed` are kept for back-compat.

Also resolve a unique chain leaf directly in help/--json-schema so a
single-family multi-segment path (e.g. `tx info`) emits its own input schema
rather than the family catalog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- generalize command `positional` → `positionals[]`; config now binds
  `[key] [value]` through the shared path, dropping the shell special-case
- rename leaf help sections Flags/Global flags → Options/Global options and
  usage placeholder [flags] → [options], aligning with root's Global Options
- fold each command's stdin channel (--*-stdin) into Options instead of a
  standalone Input options section; machine --json-schema catalog keeps
  inputFlags as a distinct key (typed args vs stdin channel stay separate)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The active chain is already known from the request — the renderer reads
`ctx.net.family` and the envelope carries `chain.family` — so duplicating
`family` inside each command's `data` was redundant. Remove it from the
TxStatusView/TxReceiptView/TxInfoView contracts (and the tx/stake/contract
service returns); the text renderer now dispatches on `ctx.net.family` via a
`renderFamily(ctx)` helper, keeping the FAMILY_RENDER table intact.

Also stop leaking the raw TronWeb broadcast response: the adapter's
`broadcast()` returned `{ txId, raw: res }`, and `raw` rode the BroadcastResult
index signature all the way into the output JSON. Return `{ txId }` only, so the
submitted/confirmed receipt exposes named fields, not the SDK's raw shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Message-sign's Ledger path never applied --timeout: MessageService.sign
awaited signer.signMessage with no bound, so an unresponsive device or an
un-tapped on-device prompt hung the CLI. Root cause was that device timeout
was a per-caller responsibility (the tx pipeline opted in, the message
service did not) while RPC calls are bounded inside their adapter.

Make the Ledger adapter self-bounding like TronChain: route every device
call through withTimeout(this.timeoutMs). Every caller now inherits the
bound, and precheck's appConfig/getAddress are covered too. --timeout is
already documented as 'per RPC/device call'; this brings the device family
in line with that contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The deployed address is deterministic at build time (createSmartContract
returns it), but nothing wired it to TxReceiptView.contractAddress, so the
text "Address" row was always empty and JSON never carried it. Capture it in
deploy()'s build callback and expose it (base58) across all stages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scrub URLs (to scheme://host), home-dir prefixes (to ~), and long hex runs
(to [redacted]) from free-text error messages before they reach the user
layer, via a new redactErrorMessage helper. Apply it to TRON RPC/broadcast/
constant-call errors. In account portfolio, stop leaking raw per-token and
price-provider errors into the success payload — degrade to stable reasons
(balanceUnavailable/priceUnavailable + reason) and render those instead of
the raw text (audit I-06).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gummy789j gummy789j force-pushed the feat/ts-version branch 2 times, most recently from 5d39dcd to 55d6967 Compare July 2, 2026 10:10
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.

2 participants