@anywalletkit/protocol-staking-lido-evm
@anywalletkit/protocol-staking-lido-evm
Section titled “@anywalletkit/protocol-staking-lido-evm”A simple package that lets @anywalletkit/wallet-evm wallet accounts stake
through the Lido protocol: stake ETH → stETH, exit through
the two-phase withdrawal queue, wrap/unwrap the non-rebasing wstETH form, and
read APR and reward history.
Install
Section titled “Install”npm install @anywalletkit/protocol-staking-lido-evm @anywalletkit/core# or: pnpm add @anywalletkit/protocol-staking-lido-evm @anywalletkit/core · yarn add @anywalletkit/protocol-staking-lido-evm @anywalletkit/coreOn npm: @anywalletkit/protocol-staking-lido-evm, published from the release workflow with provenance.
Built as the first implementation of the StakingProtocol type in
@anywalletkit/core. The base class lives in @anywalletkit/core;
src/staking-protocol.ts keeps the runtime IStakingProtocol marker and the
option / result types this module documents. See “The interface” below for
why staking is not lending.
The mechanics are extracted from a production wallet (web + React Native/Hermes) where every flow is exercised with real funds.
Written in TypeScript on viem (the only runtime
dependency) and built with tsup to dist/ (ESM + CJS + .d.ts).
import WalletManagerEvm from '@anywalletkit/wallet-evm'import LidoProtocolEvm from '@anywalletkit/protocol-staking-lido-evm'
const wallet = new WalletManagerEvm(seed, { provider: 'https://rpc.example.org' })const account = await wallet.getAccount(0)
const lido = new LidoProtocolEvm(account, { referral: '0x…' // optional: Lido Rewards-Share referral, defaults to zero address})
// Stake — stETH lands on the account 1:1const { hash, fee } = await lido.stake({ amount: 10n ** 18n })
// Exit, phase 1: queue the withdrawal (permit-first — ONE transaction)await lido.requestWithdrawal({ amount: 10n ** 18n, token: 'stETH' })
// …days later — exit, phase 2: claim finalized requests as ETHconst { claimableIds } = await lido.getWithdrawalRequests()if (claimableIds.length) await lido.claimWithdrawals({ ids: claimableIds })
// Viewsawait lido.getStakedBalance() // { balance, wrappedBalance, rate, total }await lido.getApr() // 2.72 (percent, 7d SMA)Lido-specific extras beyond the interface:
await lido.wrap({ amount }) // stETH → wstETH (approve awaited + wrap)await lido.unwrap({ amount }) // wstETH → stETH (single tx)await lido.getWithdrawalEta(ids) // Map<id, seconds> from Lido's queue APIawait lido.getRewards() // lifetime rewards + average APRawait lido.getRewardHistory({ limit: 14 }) // daily rebase events, newest firstWhy the withdrawal path looks the way it does
Section titled “Why the withdrawal path looks the way it does”Every one of these choices was paid for in production:
- Permit-first:
requestWithdrawalsigns one EIP-2612 permit and rides it insiderequestWithdrawalsWithPermit— a single transaction (and on mobile, a single biometric prompt) instead of approve + request. - Proven before broadcast: the permit path is simulated with
eth_callfirst. A bad signature must fall back, not burn gas. - The fallback awaits its approval: in the approve + request path the approval is waited to a mined receipt, because the request’s gas estimate reverts against an unmined allowance.
- Verified permit domains: stETH signs as version “2”, wstETH as version
“1” — both checked against the tokens’ on-chain
DOMAIN_SEPARATOR. A wrong version recovers to a different signer and reverts with no readable reason. - Per-request caps: the queue takes at most 1000 stETH per request; over-cap amounts split into several requests inside the same transaction (wstETH capped at 800 per request — 1000 is stETH-equivalent and the rate drifts upward).
- Up-front guards:
stakechecksisStakingPausedandgetCurrentStakeLimitbefore estimating gas, because the raw revert carries no readable reason for either. - Structural account checks: writability is
typeof account.sendTransaction === 'function', notinstanceof— a duplicated copy of the account package failsinstanceofwhile the account works fine. - No
AbortSignal.timeout/AbortSignal.any: Hermes (React Native) shipsAbortControllerbut not those statics; nothing here depends on them. - REST endpoints are configuration: APR, queue ETAs and reward history
come from Lido’s public frontend APIs (
eth-api.lido.fi,wq-api.lido.fi,reward-history-backend.lido.fi), all overridable viaconfig.endpoints. On-chain flows never depend on them: ETA and rewards are decoration and degrade to empty results, never throws.
Configuration
Section titled “Configuration”new LidoProtocolEvm(account, { provider, // RPC url or EIP-1193; defaults to the account's provider chainId, // default 1; other chains must supply the fields below referral, // forwarded to submit() — Lido Rewards-Share integrators earn on stake flow addresses, // { stEth, wstEth, withdrawalQueue } permitDomains, // { stEth: { name, version }, wstEth: { name, version } } endpoints // { apr, withdrawalQueueApi, rewardHistory }})Only mainnet ships as a default deployment — deliberately. Testnets (e.g. Hoodi) must be configured explicitly so nobody stakes against an unverified address a default quietly supplied.
The interface
Section titled “The interface”IStakingProtocol: stake / quoteStake, requestWithdrawal /
quoteRequestWithdrawal, getWithdrawalRequests, claimWithdrawals /
quoteClaimWithdrawals, getStakedBalance, getApr.
Staking is not lending: a lending withdraw is synchronous, while unstaking
is a two-phase exchange — a request enters a queue, finalizes out of band
(usually days), and only then can be claimed for the native token. Modelling
that as ILendingProtocol.withdraw would produce a method that returns with
no money received, and would stub out borrow/repay as unsupported. The
honest interface names the queue. Wrap/unwrap, ETAs and reward history stay
provider-specific extras so the core stays implementable by other liquid
staking protocols.
pnpm build # tsup → dist/pnpm test # offline unit + conformance tests (vitest, stubbed EIP-1193 provider)pnpm verify:live # read-only checks against mainnet (views, REST, eth_call simulation; needs dist/)Activity
Section titled “Activity”describeActivity(tx, { own }) labels Lido transactions for a wallet’s history, offline and synchronously. A transaction is Lido’s when it calls one of the three contracts the instance is configured with (stEth, wstEth, withdrawalQueue — mainnet by default) on that chain, and the selector is: stETH submit → stake “Staked ETH”; wstETH wrap → wrap “Wrapped stETH”, unwrap → unwrap “Unwrapped wstETH”; WithdrawalQueue requestWithdrawals / …WithPermit / …WstETH / …WstETHWithPermit → unstake “Requested withdrawal”, claimWithdrawals / claimWithdrawalsTo / claimWithdrawal → claim “Claimed ETH”. Selectors are computed from the signatures at module load (LIDO_ACTIONS). approve / transfer on stETH or wstETH are not Lido operations and yield null, as does everything else. Subtitle "Lido · Ethereum", counterparty the contract called; assetsIn / assetsOut are the wallet’s net movements from the receipt with stETH / wstETH / unstETH labelled by the module.
Fixtures in fixtures/activity/ are the test wallet’s funded run (submit, wrap, unwrap, requestWithdrawalsWithPermit, claimWithdrawals; 2026-08-31 … 09-04); negatives/ holds an SSV stake and a USDT approve.
Classes
Section titled “Classes”Interfaces
Section titled “Interfaces”- ClaimWithdrawalsOptions
- ClaimWithdrawalsResult
- Eip1193Provider
- LidoAction
- LidoActivityDeployment
- LidoDeployment
- LidoPermitDomain
- LidoProtocolConfig
- LidoTx
- RewardEvent
- RewardTotals
- StakedBalance
- StakeOptions
- StakeResult
- StakingWalletAccount
- WithdrawalRequest
- WithdrawalRequestOptions
- WithdrawalRequestResult
- WithdrawalRequestsView