Sitelet https://github.com/tronprotocol/wallet-cli/pull/1025
Skip to content

Feat/ts v4.15.0 - #1025

Open
gummy789j wants to merge 35 commits into
release_v4.15.0from
feat/ts-v4.15.0
Open

gummy789j wants to merge 35 commits into
release_v4.15.0from
feat/ts-v4.15.0

Conversation

@gummy789j

Copy link
Copy Markdown
Collaborator

feat(ts): add the SunSwap and SunPump command groups
Seventeen commands: eleven under sunswap — price, token-list, token-search,
pool-list, pool-search, position-list, position-info, swap, add-liquidity,
remove-liquidity, collect-fees — and six under sunpump — buy, sell, launch,
token-list, token-info, token-search. Liquidity covers V2, V3 mint and
increase, and V4 mint and increase; withdrawal and fee collection cover V2,
V3 and V4 as each supports them.

Everything that decides an amount was measured against a live chain rather
than reasoned about, and the measurements are in the code beside what they
justify. Four of them changed the implementation:

A V4 deposit is bounded from ABOVE where every withdrawal is bounded from
below, which means the same approximation is conservative on one protocol and
fatal on the other. Sizing from the tick's own price rather than the pool's
cost a reverted mint — the contract wanted 172953 where we offered 170007 —
and the same imprecision sat harmlessly in V3 only because V3 bounds the
other way.

A V4 withdrawal settles accrued fees alongside the principal, so the receipt
splits them. The spec says the opposite; on the transaction that proved it,
95% of what arrived was fees, and reporting only the principal would have
sent the caller looking for money already in their account.

A native V4 deposit carrying a Permit2 grant was sent with no TRX value,
because the multicall computes none of its own and every native deposit
carries a grant. The earlier live tests happened to use a token-only pool,
which is exactly the branch where it could not bite.

The create endpoint reports time in plain seconds, not the unit the spec
names. Rescaling dated a token made that minute to the year 58709, while the
query path read the same token back correctly — one field, two paths, both
answering success.

The market and curve services answer an unknown parameter by IGNORING it: a
misspelled search returns the whole catalogue with HTTP 200, and an unknown
sort field returns a plausible list in some other order while echoing the
field name back. So every parameter name and sort field is a verified
whitelist, and a value nobody measured is refused rather than forwarded.

Permit2 grants are bounded to the trade and the hour, never unlimited, and
are checked against what they were meant to authorize before a signature
exists and by recovery afterwards. The Universal Router's calldata is decoded
and checked against the same figures, because the floor, the recipient and
the deadline live in the ABI words and nowhere a caller can read.

Amounts are decimal strings and bigint throughout, every published amount
carries its scale, and a figure that could not be read publishes nothing
rather than a zero — on these commands a zero is a claim.

gummy789j and others added 30 commits September 28, 2026 15:14
Seventeen commands: eleven under `sunswap` — price, token-list, token-search,
pool-list, pool-search, position-list, position-info, swap, add-liquidity,
remove-liquidity, collect-fees — and six under `sunpump` — buy, sell, launch,
token-list, token-info, token-search. Liquidity covers V2, V3 mint and
increase, and V4 mint and increase; withdrawal and fee collection cover V2,
V3 and V4 as each supports them.

Everything that decides an amount was measured against a live chain rather
than reasoned about, and the measurements are in the code beside what they
justify. Four of them changed the implementation:

A V4 deposit is bounded from ABOVE where every withdrawal is bounded from
below, which means the same approximation is conservative on one protocol and
fatal on the other. Sizing from the tick's own price rather than the pool's
cost a reverted mint — the contract wanted 172953 where we offered 170007 —
and the same imprecision sat harmlessly in V3 only because V3 bounds the
other way.

A V4 withdrawal settles accrued fees alongside the principal, so the receipt
splits them. The spec says the opposite; on the transaction that proved it,
95% of what arrived was fees, and reporting only the principal would have
sent the caller looking for money already in their account.

A native V4 deposit carrying a Permit2 grant was sent with no TRX value,
because the multicall computes none of its own and every native deposit
carries a grant. The earlier live tests happened to use a token-only pool,
which is exactly the branch where it could not bite.

The create endpoint reports time in plain seconds, not the unit the spec
names. Rescaling dated a token made that minute to the year 58709, while the
query path read the same token back correctly — one field, two paths, both
answering success.

The market and curve services answer an unknown parameter by IGNORING it: a
misspelled search returns the whole catalogue with HTTP 200, and an unknown
sort field returns a plausible list in some other order while echoing the
field name back. So every parameter name and sort field is a verified
whitelist, and a value nobody measured is refused rather than forwarded.

Permit2 grants are bounded to the trade and the hour, never unlimited, and
are checked against what they were meant to authorize before a signature
exists and by recovery afterwards. The Universal Router's calldata is decoded
and checked against the same figures, because the floor, the recipient and
the deadline live in the ABI words and nowhere a caller can read.

Amounts are decimal strings and bigint throughout, every published amount
carries its scale, and a figure that could not be read publishes nothing
rather than a zero — on these commands a zero is a claim.
The packages went up as `@sun-protocol/sun-sdk-*` at 0.1.0-beta.0 on
2026-09-28. Until now they came from a GitLab registry on a private network
(10.90.14.25), which a GitHub-hosted runner cannot reach by design — CI never
got past `npm ci`, and npm's own message for that failure reads as a proxy
problem and sent the last reader after the wrong thing.

So `.npmrc` is gone, and with it `strict-ssl=false`. Forty-four references
across the source and the dependency-cruiser rules are renamed, and the
lockfile now resolves every package from registry.npmjs.org.

Verified against the PUBLISHED beta rather than the local checkout, which
says 1.0.0 and is not the same artefact: all 47 symbols this codebase imports
across eight packages are present. Then the whole gate, and — because
`v4-contracts.ts` passes almost everything into the SDK through `as never`,
so a changed parameter shape would compile and fail at runtime — two real
Nile transactions on a native pair, the branch where that would show:
an increase (liquidity 602323 → 1635794, 1 TRX deposited) and a withdrawal
with the fee split intact.

CI keeps a preflight, now generalised: it fails with the actual reason if any
dependency stops resolving from the public registry, rather than leaving the
next person to rediscover it from a timeout.
A V4 pool key has five parts — currency0, currency1, fee, tickSpacing,
hooks — where V3's has three. PM 6.1 carried V3's `--token0` / `--token1` /
`--fee` across to V4, which cannot name a pool there, and the gap was plugged
with a `--pool` id that the spec never had. That flag is gone. A V4 deposit
now names its pool by the key itself, with the same flags `--create-pool`
already used, so the two paths differ only by a starting price.

`--tick-spacing` is REQUIRED on V4 and has no default, because on V4 it is
part of the pool's identity rather than something the fee tier implies.
Measured on Nile: TRX/USDT at fee 500 exists TWICE, once at spacing 10 and
once at spacing 12, as two distinct pools. A default would have silently
picked one of them for anyone who did not know the other existed. With no
default, the caller chooses, and a key that names no pool is refused as
`pool_not_found` with a message that points at `--tick-spacing` first.

`pool-list` now publishes each V4 pool's decoded `tickSpacing` and its
`hooks` — "none" rather than the zero address, which on TRON is also the
native TRX marker. Before this, the spacing sat only inside a raw parameters
word and hooks were not published at all, so a caller had no way to learn
what the required flags should say.

Also fixed on the way: `--create-pool` now resolves token symbols as the
existing-pool path always did (`--token0 TRX` used to fail there), and the
tick-range help no longer claims to be V3-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A confirmed V4 mint returned no position id. Every later command names the
position by that id — increase, withdraw, collect, position-info — and on
Nile `position-list` cannot recover it, so the only record of a new position
was a transaction log the caller would have had to decode by hand. Found by
minting for real: position 178 confirmed with nothing to say it was 178.

V3 already read the id from the ERC-721 `Transfer` the position manager
emits from the zero address. A V4 mint emits the same event — measured, on
the transaction that created 178 — so the decoder is now shared and told only
which manager's log to trust. Confirmed after the fix: the next mint on Nile
prints `Position #179`.

An id that cannot be read is omitted and warned, never guessed: the deposit
is already on chain, so failing would misreport a success, and inventing an
id would send the caller to act on a position that is not theirs.

The test mock already returned an id for this call; nothing asserted it was
used. Mutation-checked: putting the empty branch back turns two cases red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while writing the command reference against the built CLI, each by
comparing what a command printed with what it did.

A V4 withdrawal pays accrued fees out alongside the principal, as V3's does,
but the dry-run warning and the help said so only for V3 — so a V4 dry run
quoted the principal and was silent about the rest.

`position-info` cut a price bound's exponent off with its fraction:
`2.939544628365392e-39` printed as `2.939544`, a full-range position's lower
bound shown 39 orders of magnitude too high and looking entirely plausible.
The shared formatter now splits the exponent off first.

A V4 deposit's dry run gave each Permit2 grant the transaction's 30-minute
deadline as its expiry, while signing a one-hour grant. It now previews the
same `now + V4_PERMIT_TTL_SECONDS` the signing path asserts.

A V4 deposit published `amountMinimum: "0"` on each side. V4 is bounded from
above and has no floor, so that zero told an agent reading the JSON the
deposit accepted any amount. It is dropped at publication only: V2 and V3
genuinely need a minimum when they send, so the internal type keeps it.

Each has a test, and each test was mutation-checked by putting the old
behaviour back and watching it fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This directory ships in the npm package, and it was behind the code.

Three pages still said V4 was unsupported: add-liquidity, remove-liquidity
and collect-fees. They now cover V4, including that a V4 pool is named by its
full key — `--tick-spacing` required, because TRX/USDT at fee 500 exists twice
on Nile at spacings 10 and 12 — that a V4 deposit is capped from above where
V2 and V3 are floored from below, and that a V4 withdrawal pays accrued fees
out with the principal.

Five commands had no page at all: sunswap position-info, and sunpump launch,
token-info, token-list and token-search. The launch page opens with the fact a
reader most needs: the token it creates is not theirs.

Every page was written from the built CLI's own `--help` and from examples
actually run — read-only commands on mainnet, anything that writes as
`--dry-run` only — rather than from the spec, which the shipped behaviour
departs from in recorded ways. The group and top-level indexes list exactly
what ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`sunswap position-info` already turned the revert an unminted id causes into
`position_not_found`, but add-liquidity, remove-liquidity and collect-fees
read the same position without that step, so the same wrong id surfaced as
`execution_reverted: TRON constant call reverted` (V3 `Invalid token ID`,
V4 `NOT_MINTED` on Nile). A caller could not tell a mistyped id from a
contract failure.

The reinterpretation moves out of position-info into a shared
`readPosition`, which every ownership check now goes through. It still
narrows only reverts: a timeout or transport failure keeps its own code.

Fixes BUG-V415-R5-004.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The creating path sizes a deposit at the caller's --sqrt-price and never
read the chain, so on a live pool it planned amounts for a price the pool
does not have and published `poolCreated: true` for a pool nobody created.
Measured on Nile: TRX/USDT at fee 500, tick spacing 10.

It now reads the pool's existence (not its price) and refuses with a new
`pool_already_exists` code that points at depositing without --create-pool.
machine-interface.md also gains the `position_not_found` row it lacked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`sunswap swap TRX USDT 0 --quote` reached the route service, whose
"INVALID AMOUNT" came back as provider_error (exit 1) — a service fault
for what is the caller's own input. Zero is now `invalid_amount` (exit 2)
before either market is consulted.

Fixes BUG-V415-R5-003.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`--wait` is global, so `sunswap swap --quote --wait` and
`sunpump launch --wait` accepted it and ignored it, reading as having
waited for a confirmation that does not exist.

- A command can now declare `rejectsWait` with a reason, mirroring
  `rejectsAccount`; `sunpump launch` does, since its token is created
  server-side and no transaction comes back.
- `sunswap swap` refuses --wait with --quote, as `tx send` already does
  with --dry-run.

Read-only commands that ignore --wait are unchanged.

Fixes BUG-V415-R5-002.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Initialize V4 pools atomically, preserve approval transaction IDs on failures, and verify received amounts from transaction receipts. Correct allowance and Permit2 planning, transaction expiry handling, Ledger cancellation and error mapping, and CLI previews and validation.

Keep V4 fee and tick spacing explicit for new positions. Add regression coverage and update command and Ledger documentation.
The Node 22 and 24 CI jobs failed Prettier checks on these three fixtures. Normalize their formatting without changing the parsed JSON data.
Fix the defects found by re-running the v4.15.0 test cases against mainnet and Nile.

SunPump
- Stop subtracting the platform fee a second time: the contract's
  getTrxAmountBySaleWithFee already returns the seller's net proceeds. This
  corrects trxOutExpected, the slippage floor and platformFeePercent.
- Derive the minimum sale from getExactTrxAmountForSaleWithFee(token, 1).
- Refuse --slippage, --min-out, --wait and --wait-timeout under --quote.
- Report account_not_active for an account that is not activated.
- Report a failed approval as failed on chain instead of as a confirmed
  approval that left no allowance.

SunSwap swap
- Check balances on the router path before planning any approval or Permit2
  grant, in every mode.
- Route the router through the timed, rate-limit-aware fetch: 429 is reported
  as provider_rate_limited, and --timeout and the response cap now apply.
- Resolve symbols through the signer's token book, and refuse ambiguous
  symbols with ambiguous_token_symbol.

SunSwap liquidity
- Keep domain error codes: invalid_option for --sqrt-price without
  --create-pool, and same_token for a V4 pair of one token.
- Publish liquidityExpected in dry-run, and positionManager as the V3/V4
  contract key.
- Refuse collect-fees with nothing to collect, consistently across modes.
- Reject an invalid --recipient as invalid_address.
- Print "Create pool" as one line with a decimal price.
- Make "No minimum set" the last dry-run line.
- Report short position data as invalid_node_response.

SunSwap queries
- Reject --account on the read-only market queries.
- position-list now selects its address with --account, like account
  balance, and drops --owner.
- Add pool-list --min-tvl: filter first, then paginate, within the market
  API's 1000-row window, warning when that window truncates the result.
- Refuse --offset + --limit beyond 1000 locally on the five list commands.

Tests
- Give the golden project its own sequence group, so `npm test` runs both
  projects under vitest 4.
Approvals and other contract calls need "Custom contracts"; Permit2 grants
and oversized transactions need "Sign by Hash". The help named only the
latter, and on three of the six write commands. Share one help line across
all six, add a TRON app settings section to the Ledger guide, and point
every command doc at it.
…only

wallet-cli kept its own copy of addresses the SDK already ships: V2 router,
V3 position manager and WTRX in the builtins, the launchpad, a V4 position
manager record cross-checked against the SDK, an unread V4 pool manager, and
a config override for the Universal Router. The V4 and router paths already
took theirs from the SDK, so two sources co-existed and every SDK bump would
have needed a matching builtin edit.

All addresses now come from @sun-protocol/sun-sdk-chains inside the outbound
adapters. The use cases ask LiquidityPort.contracts() instead of reading the
network descriptor. Values are unchanged; a unit test pins them.

Availability is now an explicit switch rather than the presence of an
address, because the SDK also knows Nile's launchpad and the release must
keep the curve off there:
- sunswap.liquidity: true  -> liquidity commands and position-info
- sunpump.curve: true      -> sunpump buy/sell and the curve branch of swap
Mainnet ships both; Nile ships liquidity only. Released behaviour is the same.

config.yaml now rejects the retired sunswap.contracts and sunpump.launchpad
fields with a pointer to the switch, and validates the sunpump block
(apiBaseUrl must be http(s)), which was previously unchecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments and test titles cited section numbers of planning documents
that are not part of the repository (PM/PRD sections, decision-log IDs,
ADR and review-finding numbers, plan task numbers, QA notes), and some
narrated release history instead of explaining the code.

Rewrite them to state the behaviour and its rationale directly, keep
measured evidence (dated Nile/mainnet observations), and translate the
few remaining non-English comments. No code or runtime output changes;
only comments, test titles and one dependency-cruiser rule message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the deprecated `ZodIssue` alias with `z.core.$ZodIssue` in the
sunpump and sunswap schema tests, and resolve the launch logo fixture
with `fileURLToPath` instead of `URL.pathname`, which percent-encodes
spaces and yields `/C:/...` on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an account already holds a Permit2 grant covering the trade, the
planner answers already-approved and produces no permit. The router
swap refused this with permit_mismatch, on the belief that the router
call must carry a permit. It does not: the SDK's own swap planner
encodes the call without one, and the router pulls through Permit2
either way. Accounts used with other SDK-built clients, whose planner
grants MAX_UINT160 for thirty days, could not swap that token.

Send the call bare in that case, sign nothing, and still check the
token's allowance to Permit2, which is a separate layer. A dry run can
now price the swap itself, and its note no longer blames a signature
when only the approval is pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V4 mint and increase took --deadline verbatim, so a past or fractional
value was signed and broadcast, then reverted on chain at the caller's
expense. Route both through resolveDeadline like every other liquidity
path. The increase tests pinned a deadline that has since passed; move
it far into the future.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owed-fees read is a static `collect`, which the position manager
only lets the owner or an approved address call. It was made from the
default zero-address reader, which passed only because an unset
`getApproved` is also zero. A token with a per-token approval reverted
with `Not approved`, breaking collect-fees (dry run included) and
dropping the fee figures from remove-liquidity and position-info.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve V4 position integrity errors, build pending approvals without checking them on chain, bound numeric expansion and token precision, and match V3 Collect events by token ID. Covers CR-003, CR-007, CR-019, CR-020, CR-030, and CR-031.
Several pages were written before later fixes landed and drifted from
the code:

- Availability: describe the sunswap.liquidity / sunpump.curve switches
  and the service URLs instead of configured contract addresses, and
  say that swap is mainnet only.
- position-list takes --account and publishes positionType; the
  sunswap overview no longer claims every query runs without a wallet.
- Dry-run fees: document the all / approvals / none cases for swap and
  V4 deposits, fix the V4 examples, and add "none" to the feeCovers
  table in machine-interface.
- Name both unlimited-approval paths (V4 Permit2 allowance and selling
  into the SunPump curve) and fix the broken cross-reference.
- Correct remove-liquidity, position-info, pool-list, pool-search and
  the sunpump pages (fee rate, approvalTxIds, launch refusals, exit
  codes), and list the sunswap/sunpump writes in the global options.
- Move sections appended after "See also" in machine-interface and the
  Ledger guide back into the body; their wording is unchanged.
- CLAUDE.md: update the per-family binding counts (TRON 96, EVM 30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The --quote table labelled every router trading fee as TRX scaled by
six decimals, but tradingFee is in base units of the input token, so a
USDT or 18-decimal input printed the wrong unit and scale. Keep TRX
only for the SunPump platform fee.

Identify native TRX by address rather than by symbol when attaching the
platform-fee note, so a token that calls itself TRX is not treated as
the native coin. The failed-swap note no longer says the TRC20 approval
expires; only the Permit2 grant does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin every @sun-protocol/sun-sdk-* package to 0.1.0 instead of
0.1.0-beta.0. The published tarballs are code-identical to the beta;
only internal dependency versions and bundler chunk ordering differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gummy789j and others added 2 commits October 8, 2026 16:55
A user's sunswap or sunpump block used to replace the builtin one
wholesale. An overlay that set only a Nile routerApiBaseUrl therefore
dropped the builtin liquidity switch, and one that overrode the mainnet
router dropped the market API and liquidity with it — features lost to
a config file that said nothing about them.

Both blocks now merge field by field: a field the user writes overrides
the builtin, a field they leave out keeps its builtin value, and turning
a feature off takes an explicit false. A block that is not a mapping is
still rejected, and so are the retired contracts/launchpad fields. The
gasfree block keeps replace semantics: its fields describe one
deployment and must not be mixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tronprotocol tronprotocol deleted a comment from baonian253-a11y Oct 8, 2026
@tronprotocol tronprotocol deleted a comment from baonian253-a11y Oct 8, 2026
@tronprotocol tronprotocol deleted a comment from baonian253-a11y Oct 8, 2026
@tronprotocol tronprotocol deleted a comment from baonian253-a11y Oct 8, 2026
@tronprotocol tronprotocol deleted a comment from baonian253-a11y Oct 8, 2026
@tronprotocol tronprotocol deleted a comment from baonian253-a11y Oct 8, 2026
gummy789j and others added 2 commits October 8, 2026 23:20
SunPump deploys the token before it answers the create call, so a call
that times out has not failed: R11 QA hit a 20s --timeout, got a plain
`timeout` (retry "same"), and the token was already on chain. A caller
that obeyed the retry hint would have created a second, permanent token.

Once the request may have reached the service, only an explicit refusal
(a non-zero envelope code, an HTTP 4xx, a rate limit) or a usage error
raised before sending passes through unchanged. Every other failure --
timeout, 5xx, dropped connection, unparseable or token-less reply -- is
now `launch_outcome_unknown` (exit 1, retry "never"), with the name and
symbol in details and a `sunpump token-search <symbol>` pointer. The old
hint pointed at `token-list --owner`, which the caller cannot use: the
owner is chosen by SunPump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V2 and V3 deposits refused an uncovered side up front, but the shared V4
sizing (mint and increase) did not. Measured on Nile through a Ledger: with
9 USDT against a 60.74 USDT side, the dry run passed, the device signed the
Permit2 grant, and only the main call's estimate then reverted with a bare
execution_reverted. A V4 dry run cannot estimate the main call, so this
check is its only signal.

Each TRC20 side is now checked against its ceiling (what Permit2 may pull),
not the deposit, before approvals are planned or anything is signed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants