diff --git a/README.md b/README.md index e074bb382..b992cb920 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Both manage the same kind of wallet on the same networks — your address is ide | **Command style** | PascalCase verbs: `RegisterWallet`, `SendCoin`, `GetBalance`. Amounts in **SUN** (1 TRX = 1,000,000 SUN). | Noun-verb subcommands: `create`, `tx send`, `account balance`, with `--flags`. | | **Output for scripts** | Human-readable text. | Stable JSON via `-o json` ([`wallet-cli.result.v1`](ts/docs/machine-interface.md)) + fixed exit codes (`0`/`1`/`2`). | | **Config / networks** | `config.conf` (net type + full node), or `SwitchNetwork` at runtime. Mainnet · Nile · Shasta · custom. | `--network` flag / `config` command. `tron:mainnet` · `tron:nile` · `tron:shasta`. | -| **Signing** | Software keystore · Ledger. | Encrypted local keystore · Ledger. Secrets never via argv/env. | +| **Signing** | Software keystore · Ledger. | Encrypted local keystore · Ledger. Secrets enter via stdin/TTY, never argv or dedicated secret env vars. | | **Feature scope** | **The full surface** — wallets and transfers, staking, voting and rewards, governance, contracts, TRC10, and the on-chain exchange. | **The full surface** — HD wallets, TRX/TRC20/TRC10 transfers, staking & delegation, voting & rewards, governance proposals & super-representative operation, contract call/deploy/governance, TRC10 issuance, the on-chain Bancor exchange, multi-sig, GasFree transfers, message signing, and on-chain queries. | | **Best for** | People at a terminal who want every TRON capability. | Scripting, CI pipelines, and AI agents. | | **Full docs** | [java/README.md](java/README.md) | [ts/README.md](ts/README.md) | diff --git a/ts/README.md b/ts/README.md index 990e47ea7..ebf8e1a18 100644 --- a/ts/README.md +++ b/ts/README.md @@ -5,7 +5,7 @@ The agent-first implementation of wallet-cli, built for automation: every comman ## Key features - **Agent-first** — stable JSON output, deterministic exit codes, and discoverable schemas, built for scripts, CI, and AI agents (details in [The contract, in one paragraph](#the-contract-in-one-paragraph)). -- **Encrypted local storage** — software keystores are encrypted on disk; secrets are never passed via argv or environment variables. +- **Encrypted local storage** — software keystores are encrypted on disk; secrets enter via stdin/TTY, never argv or dedicated secret environment variables. - **Software and Ledger signing** — sign in software, or on a Ledger device (the private key never leaves the device). - **Covers the full TRON feature surface** — HD wallets, TRX and TRC20/TRC10 transfers, staking / resource delegation, voting / rewards, governance proposals and super-representative operation, smart-contract calls, deployment and governance, TRC10 issuance, the on-chain Bancor exchange, multi-sig, GasFree transfers, message signing, and on-chain queries. @@ -174,7 +174,7 @@ Offline local commands and configuration. ## The contract, in one paragraph -Every command supports `-o json` and then prints **exactly one** terminal JSON frame on stdout, schema [`wallet-cli.result.v1`](docs/machine-interface.md#the-result-envelope). Exit codes are fixed: `0` success, `1` execution failure, `2` usage error. Secrets (passwords, mnemonics, private keys) are never accepted via argv or environment variables — only via stdin flags or interactive TTY prompts; mnemonic/private-key import and `change-password` are interactive-only (no stdin path at all). Full spec: [machine-interface.md](docs/machine-interface.md); for calling from an AI agent, see the [Agent skill](skills/wallet-cli/SKILL.md). +Every command supports `-o json` and then prints **exactly one** terminal JSON frame on stdout, schema [`wallet-cli.result.v1`](docs/machine-interface.md#the-result-envelope). Exit codes are fixed: `0` success, `1` execution failure, `2` usage error. Secrets (passwords, mnemonics, private keys) are never accepted via argv and are not read from dedicated secret environment variables. Passwords can enter through stdin flags or interactive TTY prompts; mnemonic/private-key import and `change-password` are interactive-only (no stdin path at all). Full spec: [machine-interface.md](docs/machine-interface.md); for calling from an AI agent, see the [Agent skill](skills/wallet-cli/SKILL.md). ## Understanding TRON mechanics diff --git a/ts/docs/commands/tx/broadcast.md b/ts/docs/commands/tx/broadcast.md index 20b61ea68..f386d1caa 100644 --- a/ts/docs/commands/tx/broadcast.md +++ b/ts/docs/commands/tx/broadcast.md @@ -6,7 +6,7 @@ Broadcast a presigned transaction. ``` wallet-cli tx broadcast (--hex | --file | --transaction | --tx-stdin) - [--dry-run] --network [options] + [--dry-run] [--network ] [options] ``` ## Description diff --git a/ts/docs/commands/tx/send.md b/ts/docs/commands/tx/send.md index 054c7fe4f..18706c1d1 100644 --- a/ts/docs/commands/tx/send.md +++ b/ts/docs/commands/tx/send.md @@ -32,7 +32,7 @@ a different id is refused outright), but a wrong value *inside* that range canno locally — there is nothing to compare it against. When the exact base-unit quantity matters, pass `--raw-amount`, which is used verbatim and never rescaled. -Early exits: `--dry-run` builds and estimates only — no signature, no broadcast, nothing leaves your machine; `--sign-only` signs and prints the signed transaction **hex**; `--build-only` builds but does **not** sign, printing the **unsigned** hex. The hex is protobuf on TRON and RLP (`0x02…`) on EVM; either feeds [`tx sign`](sign.md) and [`tx broadcast`](broadcast.md). +Early exits still build through the selected network first. `--dry-run` builds and estimates, then returns the plan with no signature and no broadcast; `--sign-only` builds, estimates, signs, and prints the signed transaction **hex** without broadcasting; `--build-only` builds and estimates but does **not** unlock or sign, printing the **unsigned** hex. The hex is protobuf on TRON and RLP (`0x02…`) on EVM; either feeds [`tx sign`](sign.md) and [`tx broadcast`](broadcast.md). **Fees are family-specific.** TRON burns bandwidth/energy and caps the energy spend with `--fee-limit`; EVM pays gas, so `--gas-limit`, `--max-fee`, `--priority-fee` and `--nonce` apply instead. Help tags each set `(tron only)` / `(evm only)`, and using one on the other family is refused with `invalid_option` — as are `--max-fee` / `--priority-fee` on an EVM chain that still prices in `gasPrice`. @@ -42,7 +42,12 @@ TRON multi-sig uses `--permission-id` to select the signing group and `--expirat **By default the command returns at submission** (`stage: "submitted"`), not confirmation — add `--wait` to block until confirmed/failed, or poll [`tx status`](status.md). -Requires an account and the master password via `--password-stdin` — signing commands do not show an interactive prompt, so without it the command fails with `auth_required`. +Requires: + +```text + the master password only when the selected mode signs — pass --password-stdin then; other modes need no password + an account — defaults to active; override with --account (or run `wallet-cli use ` to change the active account) +``` ## Options @@ -53,9 +58,9 @@ Requires an account and the master password via `--password-stdin` — signing c | `--raw-amount ` | Raw integer amount in native base units (SUN / wei) or token base units | | `--token ` | Token symbol from the address book; excludes `--contract`, `--asset-id` | | `--contract ` | Token contract address; omit for a native-coin transfer | -| `--dry-run` | Build and estimate only; excludes `--sign-only` / `--build-only` | -| `--sign-only` | Sign without broadcasting, output the signed hex; excludes `--dry-run` / `--build-only` | -| `--build-only` | Build only, output the **unsigned** hex; excludes `--dry-run` / `--sign-only` | +| `--dry-run` | Build and estimate through the selected network; no signing or broadcast; excludes `--sign-only` / `--build-only` | +| `--sign-only` | Build, estimate, sign, and output the signed hex without broadcasting; excludes `--dry-run` / `--build-only` | +| `--build-only` | Build and estimate, output the **unsigned** hex without unlocking; excludes `--dry-run` / `--sign-only` | | `--wait` / `--wait-timeout ` | Poll after broadcast until confirmed/failed (cap default 60000; on cap returns the submitted receipt) | | `--password-stdin` | Master password from stdin | @@ -81,7 +86,7 @@ Plus the [global options](../index.md#global-options-every-command). ## Examples -> **Password**: except for `--dry-run`, the examples below omit the password to keep the focus on the selector flags. A real send needs the master password on stdin — prefix with `printf '%s' "$PW" |` and append `--password-stdin` (see the description above). +> **Password**: the examples below omit the password to keep the focus on selector flags. Software signing modes need the master password on stdin — prefix with `printf '%s' "$PW" |` and append `--password-stdin`; `--dry-run`, `--build-only`, and Ledger signing do not. ```bash # 1 TRX on Nile; 1 ETH-denominated amount on Sepolia diff --git a/ts/docs/commands/tx/status.md b/ts/docs/commands/tx/status.md index 59aba9863..cd5f3a576 100644 --- a/ts/docs/commands/tx/status.md +++ b/ts/docs/commands/tx/status.md @@ -14,10 +14,12 @@ Reports which step a transaction is at, using **four states**, on TRON and EVM n | `data.state` | Meaning | Terminal? | |---|---|---| -| `confirmed` | On chain — solidified on TRON, receipted on EVM; `blockNumber` present | yes | +| `confirmed` | Included in a block and an execution result/receipt is available; `blockNumber` present | yes | | `failed` | Included and reverted / rejected | yes | -| `pending` | Seen by the node, not yet solidified | no — keep polling | -| `not_found` | Unknown to the queried node (wrong network? not propagated yet?) | no — poll within your own deadline | +| `pending` | Seen by the node, with no execution result/receipt yet | no — keep polling | +| `not_found` | Unknown to the queried endpoint (wrong network, not propagated, dropped, or pruned); outcome unknown | no — keep polling/reconcile; do not assume failure | + +> `confirmed` is an inclusion-and-receipt state, not a finality guarantee. If a workflow needs finality, verify it separately with a TRON SolidityNode view or an EVM finalized block. ## Options @@ -54,7 +56,7 @@ wallet-cli tx status --txid 0x55b0068ef31bce39bbf5b06d456eaef307fd77f96d85ea291f {"schema":"wallet-cli.result.v1","success":true,"command":"tx.status","data":{"txid":"0x55b0068ef31bce39bbf5b06d456eaef307fd77f96d85ea291f48c1ae4b900d80","state":"confirmed","confirmed":true,"failed":false,"blockNumber":11576586,"confirmations":0},"meta":{"durationMs":408,"warnings":[]},"chain":{"family":"evm","network":"evm:11155111","chainId":"11155111"}} ``` -An unknown txid is a **success** with `state: "not_found"` (exit 0) — the query worked; the answer is "not there": +An unknown txid is a **success** with `state: "not_found"` (exit 0) — the query worked; this endpoint has no record of that hash: ```json {"schema":"wallet-cli.result.v1","success":true,"command":"tx.status","data":{"txid":"0000…0000","state":"not_found","confirmed":false,"failed":false},"meta":{"durationMs":1022,"warnings":[]},"chain":{"family":"tron","network":"tron:nile","chainId":"nile"}} @@ -66,6 +68,8 @@ On EVM, `not_found` also carries a `meta.warnings` entry, because a public endpo {"…":"…","data":{"txid":"0x0000…0000","state":"not_found","confirmed":false,"failed":false},"meta":{"durationMs":407,"warnings":["0x0000…0000 is unknown to this endpoint. Public nodes often prune history, so this may mean the node has no record of it rather than that it never existed; try an archival endpoint."]}} ``` +> A polling deadline that ends in `pending` or `not_found` is still an unknown outcome. Do not treat it as failure or use it as an automatic resend trigger; reconcile the txid against the intended network and endpoint history first. + ## Output | Field | Type | Meaning | diff --git a/ts/docs/concepts/security.md b/ts/docs/concepts/security.md index 56d77ee39..8dad1ebd7 100644 --- a/ts/docs/concepts/security.md +++ b/ts/docs/concepts/security.md @@ -14,7 +14,7 @@ One seed covers **every chain family** — the same phrase re-derives your TRON ## Secrets in transit: stdin or TTY, never argv/env -Anything in a command's arguments or environment leaks into shell history, `ps` output, and CI logs. wallet-cli therefore refuses secrets there — they enter only via: +Anything in a command's arguments leaks into shell history and `ps` output. Exported environment variables are also easy to leak through shells and CI logs. wallet-cli therefore refuses passwords, mnemonics, and private keys in argv and does not read any dedicated secret environment variables for them — they enter only via: - interactive TTY prompts, or - explicit stdin flags: `--password-stdin`, `--tx-stdin` — **one `*-stdin` flag per run**, so a pipeline can never silently feed the wrong secret to the wrong prompt. The highest-value secrets go further: mnemonics and private keys are accepted **only** via hidden TTY input (`import mnemonic` / `import private-key` / `change-password` have no stdin path at all). @@ -46,7 +46,7 @@ Unexpected internal exceptions are collapsed to a generic `internal_error` messa | Software key | `create` / `import` | Convenient; host compromise = key compromise | | Ledger | `import ledger` | Key never on host; every send confirmed on-device. `--app` fixes the account to one chain family — import once per app to cover both — see [Ledger guide](../guide/ledger.md) | | Watch-only | `import watch` | No signing at all; safe for monitoring balances of cold storage. Bound to the pasted address's family | -| Split sign/broadcast | `--sign-only` + `tx broadcast` | Signing machine needs no network — see [Scripting](../guide/scripting.md#sign-here-broadcast-there) | +| Split sign/broadcast | `tx send --build-only` → `tx sign --offline` → `tx broadcast` | Signing machine can stay offline; `--sign-only` still builds and estimates online — see [Scripting](../guide/scripting.md#sign-here-broadcast-there) | ## What wallet-cli cannot do for you diff --git a/ts/docs/guide/scripting.md b/ts/docs/guide/scripting.md index 98c737737..7c2cc80fd 100644 --- a/ts/docs/guide/scripting.md +++ b/ts/docs/guide/scripting.md @@ -35,14 +35,14 @@ else fi ``` -**3. Secrets via stdin, never argv.** Passwords/mnemonics/keys in arguments would end up in shell history and `ps` output: +**3. Secrets via stdin, never argv.** Passwords/mnemonics/keys in arguments would end up in shell history and `ps` output. wallet-cli does not read dedicated secret environment variables either: ```bash printf '%s' "$PW" | wallet-cli tx send --to T... --amount 1 \ --network tron:nile --password-stdin -o json ``` -(`$PW` should come from your secret store, not from a file in the repo. Only one `*-stdin` flag per run.) +(`$PW` should come from your secret store as a short-lived shell variable for this pipe, not from a file in the repo and not from a long-lived `export`. Only one `*-stdin` flag per run.) ## Waiting for confirmation @@ -57,18 +57,24 @@ Or decouple: capture `data.txId`, then poll [`tx status`](../commands/tx/status. ## Sign here, broadcast there -`--sign-only` and `tx broadcast` split signing from submission, so the machine holding keys never needs chain access: +`--sign-only` separates signing from broadcast, but it still builds and estimates through the selected RPC endpoint before signing. For a signing machine with no chain access, build unsigned hex online, sign that artifact offline, then broadcast from an online machine: ```bash -# on the signing machine +# on the connected build machine wallet-cli tx send --to T... --amount 1 --network tron:nile \ - --password-stdin --sign-only -o json | jq -r '.data.hex' > signed.hex + --build-only -o json | jq -r '.data.hex' > unsigned.hex + +# on the offline signing machine +printf '%s' "$PW" | wallet-cli tx sign --file unsigned.hex --network tron:nile \ + --offline --password-stdin --out signed.hex # on the connected machine wallet-cli tx broadcast --file signed.hex --network tron:nile -o json ``` -The **hex** form above works on both chain families — protobuf on TRON, RLP on EVM. The JSON form is TRON-only: +The **hex** form above works on both chain families — protobuf on TRON, RLP on EVM. If the signing machine does have RPC access and you only want to withhold broadcast, `tx send --sign-only` emits signed hex directly. + +TRON also accepts signed transaction JSON, but JSON must go through `--transaction` or `--tx-stdin`; `--file` and `--hex` are hex-only: ```bash wallet-cli tx send ... --sign-only -o json | jq -c '.data.signed' > signed.json diff --git a/ts/docs/machine-interface.md b/ts/docs/machine-interface.md index 48dd97cfd..3875aa443 100644 --- a/ts/docs/machine-interface.md +++ b/ts/docs/machine-interface.md @@ -248,11 +248,13 @@ Alongside it, an error may carry a scalar list of just the identifiers to retry ## Secret handling -Secrets never travel via argv or environment variables — they would leak into shell history and process listings. Two channels only: +wallet-cli never reads passwords, mnemonics, or private keys from argv or from dedicated secret environment variables. Arguments and exported environment values leak into shell history, process listings, and CI logs. For secrets, use these CLI channels: -1. **stdin flags** — `--password-stdin`, `--tx-stdin`, `--message-stdin`. **Only one `*-stdin` flag can consume stdin per run.** (Mnemonics and private keys have no stdin path — `import mnemonic` / `import private-key` / `change-password` are interactive-only, hidden TTY input.) +1. **stdin flags** — `--password-stdin` for the master password; `--tx-stdin` / `--message-stdin` for large payloads. **Only one `*-stdin` flag can consume stdin per run.** (Mnemonics and private keys have no stdin path — `import mnemonic` / `import private-key` / `change-password` are interactive-only, hidden TTY input.) 2. **Interactive TTY prompt** — when running with a terminal attached. +Shell variables in examples are only a shell-side source for a pipe; wallet-cli does not read them. Keep them process-local and short-lived, and do not export them long term. + ```bash # non-interactive unlock printf '%s' "$MASTER_PASSWORD_FROM_YOUR_VAULT" | wallet-cli tx send \ @@ -280,21 +282,56 @@ This is a wallet; a wrong success check loses money. The rules: | `data.state` | Meaning | Terminal? | |---|---|---| - | `confirmed` | Solidified on chain (`blockNumber` present) | yes | + | `confirmed` | Included in a block and an execution result/receipt is available (`blockNumber` present) | yes | | `failed` | Included and reverted / rejected | yes | - | `pending` | Seen but not yet solidified | no — keep polling | - | `not_found` | Unknown to the queried node | no — keep polling until your own deadline, then treat as failed | + | `pending` | Seen by the node, with no execution result/receipt yet | no — keep polling | + | `not_found` | Unknown to the queried endpoint | no — keep polling/reconcile; do not assume failure | `data.confirmed` and `data.failed` are provided as booleans for direct branching. + > `confirmed` means included and receipted, not finalized. Use a TRON SolidityNode view or an EVM finalized block check when that distinction matters. + + > A deadline that ends in `pending` or `not_found` is an unknown outcome. Do not record it as failed, and do not resend automatically without external reconciliation. + **GasFree transfers are the exception.** `gasfree transfer` submits to a provider, not directly to a node: the submitted receipt carries a `traceId` (not a `txId`), and progress follows the provider's states — `WAITING` → `INPROGRESS` → `CONFIRMING` → `SUCCEED` / `FAILED`. Follow it with `--wait` or [`gasfree trace `](commands/gasfree/trace.md) rather than `tx status`; a `txId` appears only once the provider puts it on-chain. ```bash -txid=$(wallet-cli tx send --to T... --amount 1 --network tron:nile --password-stdin -o json \ - < pw.fifo | jq -r '.data.txId') || exit 1 -until [ "$(wallet-cli tx status --txid "$txid" --network tron:nile -o json | jq -r '.data.state')" = confirmed ]; do - sleep 3 # add your own deadline; 'failed' should abort, not loop +#!/usr/bin/env bash +set -euo pipefail + +deadline=$((SECONDS + 90)) +txid=$( + printf '%s' "$PW" | + wallet-cli tx send --to T... --amount 1 --network tron:nile --password-stdin -o json | + jq -er '.data.txId' +) + +while (( SECONDS < deadline )); do + state=$( + wallet-cli tx status --txid "$txid" --network tron:nile -o json | + jq -er '.data.state' + ) + + case "$state" in + confirmed) + exit 0 + ;; + failed) + echo "transaction failed: $txid" >&2 + exit 1 + ;; + pending|not_found) + sleep 3 + ;; + *) + echo "unexpected transaction state: $state" >&2 + exit 1 + ;; + esac done + +echo "transaction outcome unknown after deadline: $txid" >&2 +exit 1 ``` 4. **Batch operations**: each command is one transaction with one exit code. Stop-on-first-failure is the default safe posture; if you continue, track per-item txids and reconcile with `tx status` before reporting success. diff --git a/ts/docs/troubleshooting.md b/ts/docs/troubleshooting.md index 011ef5d01..f4b6f2210 100644 --- a/ts/docs/troubleshooting.md +++ b/ts/docs/troubleshooting.md @@ -70,7 +70,7 @@ An unexpected failure. The message is intentionally generic (secret-redaction). ## Not an error code, but frequently asked -- **`tx status` says `pending` for a long time** — the tx is seen but not solidified; keep polling. If it never leaves `pending`/`not_found` past your deadline, treat it as failed and investigate on a block explorer before resending. +- **`tx status` says `pending` for a long time** — the tx is seen, but no execution result/receipt is available yet; keep polling. If it never leaves `pending`/`not_found` past your deadline, the outcome is unknown, not failed. Reconcile it on the intended network, preferably with an explorer or archival endpoint, before any resend. - **"only one *-stdin flag can consume stdin per run"** — pipe one secret per invocation; for send-with-password use `--password-stdin` and let the mnemonic/key live in the encrypted store. - **Forgot the master password** — there is no recovery; restore from your BIP39 mnemonic (`import mnemonic`) into a fresh wallet and set a new password. - **`account history` fails while other queries work** — history requires a TronGrid endpoint; plain node RPC is not enough. It is also TRON-only: on an EVM network it fails with `family_mismatch`.