@anywalletkit/protocol-staking-tonstakers-ton
v0.3.0
Published
A simple package that lets @anywalletkit/wallet-ton wallet accounts stake through Tonstakers: stake GRAM → tsTON, unstake instant / standard / best-rate, track payout bills, rates and APY.
Maintainers
Readme
@anywalletkit/protocol-staking-tonstakers-ton
A simple package that lets @anywalletkit/wallet-ton wallet accounts stake
through Tonstakers: stake GRAM → tsTON, exit
instant / standard / best-rate, track payout bills, read rates, APY and
earnings.
Install
npm install @anywalletkit/protocol-staking-tonstakers-ton @anywalletkit/core
# or: pnpm add @anywalletkit/protocol-staking-tonstakers-ton @anywalletkit/core · yarn add @anywalletkit/protocol-staking-tonstakers-ton @anywalletkit/coreOn npm: @anywalletkit/protocol-staking-tonstakers-ton, published from the release workflow with provenance.
Built as the third implementation of the StakingProtocol type in
@anywalletkit/core — after Lido (ETH liquid staking) and SSV (token staking
with a cooldown) — and the first one off EVM. The base class in
src/staking-protocol.js is the same file, byte-identical; a TON liquid
staking protocol fitting it is the point: the type is chain-agnostic.
The mechanics are extracted from a production wallet (React Native/Hermes) where every flow was exercised with real funds.
What Tonstakers and tsTON are
The Open Network's native token is GRAM (Toncoin was renamed Gram on 2026-06-15; the chain keeps the TON name). Tonstakers is TON's largest liquid-staking pool (~128M GRAM, ~140k stakers): the pool contract lends deposited GRAM to validators, and tsTON is the non-rebasing receipt jetton whose GRAM value grows with the pool's rewards — the rate is the pool's balance over the jetton supply and moves once per validation round (~18h). Exits burn tsTON in one of three modes: standard (paid now from the pool's free liquidity when there is enough, otherwise a payout bill settled after the round), instant (paid now or the message bounces — never a bill), best rate (always a bill, settled at the round-end projected rate). Bills are NFTs the pool pays out by itself; there is nothing to claim.
Usage
import WalletManagerTon from '@anywalletkit/wallet-ton'
import TonstakersProtocolTon from '@anywalletkit/protocol-staking-tonstakers-ton'
const wallet = new WalletManagerTon(seed, { tonClient: { url: 'https://toncenter.com/api/v2/jsonRPC' } })
const account = await wallet.getAccount(0)
const tonstakers = new TonstakersProtocolTon(account, {
partnerCode: 0x…n // optional: Tonstakers partner code for referral attribution
})
// Stake — tsTON lands at the pool rate; the message carries +1 GRAM the pool refunds
const { hash, fee } = await tonstakers.stake({ amount: 10n ** 9n })
// Exit: standard (default), instant (fill-or-kill) or bestRate (round-end rate)
await tonstakers.requestWithdrawal({ amount: 10n ** 9n, mode: 'instant' })
// Bills waiting for their round (settled automatically — nothing to claim)
const { requests, pendingAmount } = await tonstakers.getWithdrawalRequests()
// Views
await tonstakers.getStakedBalance() // { balance: tsTON, rate: GRAM per tsTON, total: GRAM value }
await tonstakers.getApr() // 12.86 (percent, tonapi)Tonstakers-specific extras beyond the interface:
await tonstakers.getPoolData() // halted, depositsOpen, balances, minter, payout collection, rates
await tonstakers.getRates() // { rate, projectedRate } — GRAM per tsTON, 1e9 fixed
await tonstakers.getInstantLiquidity() // GRAM the pool can pay instant exits from
await tonstakers.getJettonWallet() // the account's tsTON jetton wallet (derived, checked against the minter)
await tonstakers.getTokenBalance() // tsTON held
await tonstakers.getRewards() // earned GRAM = (rate − weighted entry rate) × tsTONHow it maps onto the interface
| IStakingProtocol | Tonstakers |
|---|---|
| stake({ amount }) | one message to the pool: amount + 1 GRAM, body op 0x47d54391 · query_id 1 · partner code |
| requestWithdrawal({ amount, mode }) | burn on the account's tsTON jetton wallet: 1.05 GRAM attached, body op 0x595f07bc · amount · owner · {wait_till_round_end, fill_or_kill} |
| getWithdrawalRequests() | the account's payout-bill NFTs in the pool's active payout collections (claimable always false) |
| claimWithdrawals() | rejects — bills are settled by the pool, not claimed |
| getStakedBalance() | tsTON balance, rate GRAM/tsTON, total = GRAM value |
| getApr() | tonapi's staking-pool view (the number Tonstakers shows) |
Three honest deviations, documented on the methods rather than papered over:
the exit has modes (an extra mode option), the second phase is
automatic (so claimWithdrawals refuses instead of pretending), and
total is the position's value in GRAM — for a non-rebasing receipt the
GRAM value is the figure that means anything.
Why it looks the way it does
- The official SDK only speaks TonConnect (
tonstakers-sdkbuilds and sends through a connector; no payload export). Its message builders — op codes, fee reserves, payload layout — are ported verbatim and ride the account's ownsendTransaction({ to, value, body }), which signs locally. - Friendly addresses on the wire: the module hands the account
EQ…forms, which carry the bounce flag; a raw0:…jetton-wallet address carries none. - Pool state is read on-chain (
get_pool_full_data, layout verified against mainnet), through the account's TonClient with failover across configured clients. Public toncenter nodes intermittently answer exit code −13 on that getter, so each client gets two tries. - Instant exits are pre-checked against the pool's free balance (the contract would bounce them anyway, but a bounce carries no readable reason).
- REST is decoration: bills and APY come from the indexes the official
SDK reads (tonapi, Tonstakers' payout API), overridable or disable-able via
config.endpoints; on-chain flows never depend on them, andgetRewardsdegrades to null. - No
AbortSignal.timeout/.any, noprocess.env— Hermes and Bare friendly. Mainnet and testnet both ship pool and tsTON minter defaults. - The jetton wallet is derived, not trusted: the account's tsTON jetton
wallet — the destination of every unstake's 1.05 GRAM — is computed on the
device from the minter's pinned wallet code (
TONSTAKERS_JETTON_WALLET_CODES,deriveTonstakersJettonWallet), and the minter'sget_wallet_addressmust agree or the call is refused. A minter with no pinned code (a customaddresses.jetton) needsjettonWalletCode, or the explicitallowUnverifiedJettonWallet: trueto trust the node's answer.
Configuration
new TonstakersProtocolTon(account, {
tonClient, // TonClient | { url, secretKey } | array of either; defaults to the account's
network, // '-239' mainnet (default) or '-3' testnet
addresses, // { pool, jetton }
jettonWalletCode, // Cell | base64 BOC — wallet code of a minter not pinned in the package
allowUnverifiedJettonWallet, // true: no known code, trust get_wallet_address (off by default)
partnerCode, // referral attribution in stake payloads
endpoints // { tonapi, withdrawalPayouts } — null disables the feature
})Mainnet contracts (verified live): pool
EQCkWxfyhAkim3g2DjKQQg8T5P4g-Q1-K_jErGcDJZ4i-vqR, tsTON minter
EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav.
Tests
npm test # offline unit tests (node:test, stubbed TonClient + account)
npm run verify:live # read-only checks against mainnet (pool getter, jetton wallet, APY, bills, message builders)Activity
describeActivity(tx, { own }) labels Tonstakers transactions for a wallet's history, offline and synchronously, against the deployment the instance is configured with (pool + tsTON jetton master; addresses are compared in raw wc:hex form, so friendly and raw spellings both match). Three shapes: a message to the pool whose op is OP_STAKE 0x47d54391 (or undecoded) → stake "Staked GRAM" (net GRAM out — the fee reserve the pool returns is netted — tsTON minted in); a burn (op OP_UNSTAKE 0x595f07bc, read from call.data as hex / decimal or from call.method) with a tsTON jetton transfer leaving the wallet → unstake "Requested unstake" (the destination is the wallet's own jetton wallet, which cannot be known statically, so the jetton movement is the proof); an incoming message from the pool carrying GRAM (op 0x0a77535c tonstake_pool_withdrawal when decoded, no call of ours) → claim "Received unstaked GRAM". Subtitle "Tonstakers · TON", counterparty the pool. Anything else — plain GRAM or jetton transfers, other pools — is null.
Fixtures in fixtures/activity/ are the test wallet's funded run (stake 3 GRAM, unstake 1 tsTON, the settled payout; 2026-09-04 … 10-07, amounts from tonapi events and raw transactions); negatives/ holds a GRAM transfer in and a USD₮ jetton transfer out.
