@steerprotocol/sdk 3.8.1 → 4.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,509 +1,1777 @@
1
- # Steer Protocol SDK
1
+ # Next SDK scaffold
2
2
 
3
- A TypeScript SDK for interacting with Steer Protocol services.
3
+ `SteerClient` is the SDK entry point. Chains describe where Steer and its integrations run; they
4
+ don't own a selectable protocol. The built-in catalog contains all 53 chains registered by the prior
5
+ SDK, with their effective Steer contract addresses, start blocks, and subgraph URLs.
6
+ Uniswap V3 on Base supports verified pool reads, pool discovery by family and factory, and single-pool exact-input quotes, plus indexed pool listing. Steer
7
+ vault resolution and position/accounting reads are available for mapped Base and Polygon implementations.
8
+ Deposit and withdrawal previews and unsigned preparation are available for supported liquidity vaults.
9
+ Transaction execution is application-owned.
4
10
 
5
- ## Installation
11
+ ## Use Steer
6
12
 
7
- ```bash
8
- npm install @steerprotocol/sdk viem
9
- # or
10
- yarn add @steerprotocol/sdk viem
11
- # or
12
- pnpm add @steerprotocol/sdk viem
13
- ```
13
+ Initialization takes options; consumers don't pass a deployment catalog as a positional argument:
14
14
 
15
- ## Usage
15
+ ```ts
16
+ const steer = new SteerClient({
17
+ transports: { [ChainId.Base]: rpcTransport },
18
+ });
19
+ ```
16
20
 
17
- ```typescript
18
- import { createPublicClient, createWalletClient, http } from 'viem';
19
- import { mainnet } from 'viem/chains';
20
- import { VaultClient, StakingClient } from '@steerprotocol/sdk';
21
+ Chain metadata is available immediately, without an RPC transport:
21
22
 
22
- // Create viem clients
23
- const publicClient = createPublicClient({
24
- chain: mainnet,
25
- transport: http(),
26
- });
23
+ ```ts
24
+ const steer = new SteerClient();
27
25
 
28
- const walletClient = createWalletClient({
29
- chain: mainnet,
30
- transport: http(),
31
- });
26
+ const chains = steer.getChains(); // All 53 registered networks, including legacy testnets.
27
+ const subgraphUrl = steer.getSubgraphUrl(ChainId.Base);
28
+ const vaultRegistry = steer.getDeployment(ChainId.Base).contracts.VaultRegistry;
29
+ // vaultRegistry.address and vaultRegistry.startBlock (bigint when known)
30
+ ```
32
31
 
33
- // Initialize clients
34
- const vaultClient = new VaultClient(publicClient, walletClient, 'production');
35
- const stakingClient = new StakingClient(publicClient, walletClient);
32
+ `ChainId.Ethereum` and the legacy name `ChainId.Mainnet` both select chain 1. Each network appears
33
+ only once in `getChains()`. Chain metadata also retains transaction type and wrapped native token
34
+ address, plus its symbol and wrapping flag where the prior SDK supplied them.
36
35
 
37
- // Use vault operations (including single-asset deposits)
38
- const vaults = await vaultClient.getVaults({ chainId: 1 });
36
+ Consumers use these accessors (lending still needs an integration):
39
37
 
40
- // Use staking operations
41
- const pools = await stakingClient.getStakingPools();
38
+ ```ts
39
+ const vault = await steer.getVault(ChainId.Base, vaultAddress);
40
+ const amm = steer.getAmm({ chainId: ChainId.Base, protocol: AmmProtocol.Uniswap, version: UniswapVersion.V3 });
41
+ const lending = steer.getLending(aaveV3Base);
42
+
43
+ steer.getSubgraphUrl(ChainId.Base);
44
+ steer.getDeployment(ChainId.Base).contracts.VaultRegistry;
45
+ amm.subgraphUrl;
46
+ lending.subgraphUrl;
42
47
  ```
43
48
 
44
- ## Supported Chains, Subgraphs, and Protocols
49
+ Base V3/Integral and Polygon V3/Legacy/Integral vault reads are implemented below. Lending loaders still need their integration
50
+ implementations. The Steer catalog supplies network metadata and indexed vault discovery. Networks without resolver bindings report `NotConfigured` when loading a vault. Consumers don't assemble a chain or pass a `steer: steerBase` setting.
51
+ SDK integration code owns Steer's per-network contracts and data adapters.
45
52
 
46
- The SDK exposes a capabilities registry so integrators can discover supported chains and the relevant configuration (subgraph URLs, protocol availability, deployments) without enumerating enums.
53
+ ## Vault discovery
54
+
55
+ List indexed vault addresses, then load a result through the normal identity-verified resolver:
47
56
 
48
57
  ```ts
49
- import { getSupportedChains, getSupportedChainIds } from '@steerprotocol/sdk';
58
+ const page = await steer.listVaults({
59
+ chainId: ChainId.Base,
60
+ amm: { protocol: AmmProtocol.Uniswap, version: UniswapVersion.V3 },
61
+ token: { symbol: "USDC" },
62
+ limit: 20,
63
+ });
50
64
 
51
- // Most lightweight: chains that have Steer subgraph config
52
- const supportedChainIds = getSupportedChainIds();
65
+ for (const address of page.items) {
66
+ const vault = await steer.getVault(ChainId.Base, address);
67
+ }
53
68
 
54
- // Opt in to additional details (deployments, protocol subgraphs, etc)
55
- const supported = getSupportedChains({
56
- include: {
57
- subgraph: true,
58
- deployments: true,
59
- protocols: true,
60
- singleAssetDeposit: true,
61
- },
62
- // Used for protocol-specific subgraph discovery (The Graph / Studio)
63
- theGraphApiKey: process.env.SUBGRAPH_STUDIO_KEY ?? '',
64
- });
69
+ // Load a page or any explicit address list through the same resolver.
70
+ const vaults = await steer.getVaults(ChainId.Base, page.items);
65
71
 
66
- for (const chain of supported) {
67
- console.log({
68
- chainId: chain.chainId,
69
- chain: chain.chain,
70
- subgraphUrl: chain.subgraphUrl,
71
- protocolCount: chain.protocols?.length ?? 0,
72
- features: chain.features,
72
+ if (page.nextCursor) {
73
+ const next = await steer.listVaults({
74
+ chainId: ChainId.Base,
75
+ cursor: page.nextCursor,
76
+ limit: 20,
73
77
  });
74
78
  }
75
79
  ```
76
80
 
77
- ### Available Features
81
+ `amm` and `token` are optional. Token filters accept `address`, `symbol`, and `name`;
82
+ symbol and token name use case-insensitive substring matches. Combined criteria must match
83
+ the same token on either side. `name` refers to the underlying token, not the vault's name.
84
+ AMM selection uses the same typed protocol/version/deployment selectors and ambiguity rules as
85
+ `getAmm()`. Qualified AMM discovery mappings currently cover Uniswap V3 and QuickSwap Integral
86
+ on Base, plus Polygon Uniswap V3, SushiSwap V3, Retro V3, QuickSwap V3 and QuickSwap V4.
87
+ Other AMM filters fail explicitly until their exact indexed beacon mappings are qualified.
88
+
89
+ Listing costs zero RPC requests: two HTTP requests for the first page (snapshot and vaults),
90
+ then one per continuation. Cursors retain filters and pin every page to the indexed block and
91
+ subgraph deployment. If supplied again, filters must agree with the cursor. Results are indexed
92
+ candidates, which may include implementations unsupported by `getVault()`; listing does not
93
+ establish current onchain identity or add resolver support. Indexing, HTTP, and GraphQL errors propagate.
94
+
95
+ `steer.getDeployment(chainId).listVaults(request, options)` uses the same listing path. Both
96
+ entry points honor client `indexers`/`http` configuration and accept per-request `fetch`, headers,
97
+ timeout, and cancellation overrides as the second argument. The existing `getVault()` API and
98
+ its onchain identity checks remain the loading path.
99
+
100
+ `getVaults(chainId, addresses, options)` returns the same resolved vault objects as `getVault()`
101
+ in input order, including duplicate inputs. Base V3/Integral resolution uses a bulk Solidity reader:
102
+ **two RPC requests per page of at most 64 vaults** at a pinned block. Latest reads add one shared
103
+ head lookup for the entire invocation. The first call verifies original proxy and implementation
104
+ code hashes; the second uses code-only vault-address overrides to verify private beacon/registry
105
+ storage, exact implementations, pool registration and token ordering. It does not fetch accounting snapshots.
106
+
107
+ Single `getVault()` and `getLiquidityVault()` lookups use the same reader with one input: **2 pinned
108
+ RPCs or 3 for latest** on supported Base and Polygon deployments. Kind checks, adapter instances,
109
+ custom underlying readers and constructors are preserved. If the provider explicitly rejects bulk
110
+ overrides, resolution falls back directly to the original single reader, with no recursive bulk
111
+ probe. Base V3/Integral compatibility costs 15/13 pinned RPCs after first-stage rejection, or 16/14
112
+ after second-stage rejection. Latest adds one. These fallback counts include rejected probes.
113
+
114
+ Registered adapters and constructors remain in use. Custom underlying readers still execute and
115
+ may add RPC requests. Providers explicitly rejecting state overrides use the existing single-vault
116
+ resolver, with at most four active loads and no repeated bulk probe during that invocation.
117
+ Reverts, identity failures, malformed responses, timeouts and rate limits propagate without fallback.
118
+ For a pinned V3 + Integral pair, compatibility uses 27 calls after preflight rejection or 28 after
119
+ second-stage rejection, including probes. Empty input returns an empty array without IO. A failed
120
+ resolution rejects the batch without partial results or starting later pages. The deployment-level
121
+ equivalent is `steer.getDeployment(chainId).getVaults(addresses, options)`.
122
+
123
+ ## Bulk vault accounting snapshots
78
124
 
79
- - **Vault Operations**: Traditional vault deposits, withdrawals, and balance checking
80
- - **Single-Asset Deposits**: Deposit one token and automatically balance for LP provision
81
- - **Staking**: Interact with staking pools for rewards
82
- - **Pool Data**: Fetch pool information and statistics
83
- - **Smart Rewards**: Manage reward campaigns and claims
125
+ ```ts
126
+ const snapshots = await steer.getVaultSnapshots(
127
+ ChainId.Base,
128
+ vaultAddresses,
129
+ { blockNumber }, // Optional; latest selects one shared block for the entire invocation.
130
+ );
84
131
 
85
- ## Pool Creation and Initialization
132
+ snapshots[0]?.total.token0.formatted;
133
+ snapshots[0]?.fees.net.token1.raw;
134
+ ```
86
135
 
87
- Pool lifecycle support is exposed as signer-neutral primitives rather than methods on `PoolClient`. Lifecycle capability is explicit and fail-closed; protocol discovery alone never enables a write path.
136
+ The deployment-level `steer.getDeployment(chainId).getVaultSnapshots(addresses, options)` is
137
+ identical. Results use the same `LiquidityVaultSnapshot` model as `vault.getSnapshot()`, in input
138
+ order, including duplicates. This method resolves identity and reads accounting; `getVaults()`
139
+ continues to return vault objects without accounting. Supported built-in mappings are Base V3/Integral and the Polygon deployments described below.
140
+ Other vault categories and implementations fail explicitly.
141
+
142
+ Small pinned pages use **two RPC requests for up to 16 vaults**, including resolution. The first
143
+ code-only coordinator override verifies original proxy and implementation code against exact
144
+ beacon mappings and counts configured positions through original vault views. The second installs
145
+ family-local code at the verified vault addresses and reads stored identities, pool registration,
146
+ token decimals, weights, idle/reserved balances, principal and fees at the same block. Only code is
147
+ overridden; storage, balances, implementations, pools, tokens and registry code remain untouched.
148
+
149
+ Preflight position counts split execution into subbatches with a **128-position soft budget**.
150
+ Each page costs one preflight plus one request per subbatch. A vault exceeding that position budget
151
+ runs alone through the existing family reader, retaining support for large configurations. This is
152
+ scheduling, not a protocol position limit. Latest adds one head lookup for the entire invocation.
153
+ The measured batch-size rationale and coverage limits are in [the snapshot benchmark](benchmarks/vault-snapshots.md).
154
+
155
+ The actual implementation bindings, custom underlying readers, constructors, vault `getSnapshot()`
156
+ methods and AMM `getPool()` overrides still execute. Both paths first obtain family accounting, then
157
+ validate the registered vault method's output against that snapshot's block, pool, token identity/decimals
158
+ and configured position count. AMM overrides retain their existing semantics. Calling the default vault
159
+ method reuses the already verified snapshot without another RPC; an override without `super` still
160
+ receives the same identity checks. Raw-reader and completed-snapshot reuse stay internal to the
161
+ invocation; retained options expire on success/failure and concurrent calls remain isolated.
162
+
163
+ Only an explicit unsupported-override rejection selects compatibility, using the existing single-vault
164
+ resolver and snapshot paths with at most four active loads. Unsupported bulk mode is probed once per
165
+ invocation. Required reverts, identity/accounting errors, malformed responses and transport failures
166
+ propagate without retrying individual members. Empty input does no IO; malformed addresses fail
167
+ before RPC. A failure rejects the whole call and does not schedule later pages. These are accounting
168
+ snapshots, not withdrawal quotes; no Graph credentials or transactions are involved.
169
+
170
+ ## Bulk pool snapshots
171
+
172
+ Read multiple pools using the same references accepted by `getPool()`:
88
173
 
89
- The explicit lifecycle registry covers all currently configured Uniswap V3-compatible, Uniswap V4, and Algebra-family chain/protocol pairs: 104 deployments in total. It includes exact bigint price encoding, block-pinned preflight inspection, family-specific unsigned builders, sender-aware simulation, and receipt/postcondition verification. V3 factory-only and Algebra standard-pool flows remain explicitly two-step where no atomic initializer is proven.
174
+ ```ts
175
+ const amm = steer.getAmm({
176
+ chainId: ChainId.Base,
177
+ protocol: AmmProtocol.Uniswap,
178
+ version: UniswapVersion.V3,
179
+ });
180
+ const pools = await amm.getPools([poolAddress, anotherPoolAddress], { blockNumber });
181
+
182
+ // Let the family discover each pool's registered AMM.
183
+ const discovered = await steer.getPools({
184
+ chainId: ChainId.Base,
185
+ family: AmmFamily.UniswapV3,
186
+ addresses: [poolAddress, anotherPoolAddress],
187
+ blockNumber,
188
+ });
189
+ discovered[0]?.amm;
190
+ ```
90
191
 
91
- See the [detailed pool-lifecycle guide](https://github.com/SteerProtocol/sdk/blob/master/src/pool-lifecycle/README.md) for the complete flow and API boundaries.
192
+ `getPools()` returns full snapshots in input order, including duplicates. It reads one block
193
+ for the whole invocation, returns an immutable result array, and rejects the batch if any
194
+ required read fails. Empty input requires no RPC. `listPools()` still performs indexed search;
195
+ use `amm.getPools(page.items)` to load a listing page.
92
196
 
93
- ## Staking Client
197
+ All implemented AMMs support bulk snapshots: Uniswap V3/V4, PancakeSwap V3/Infinity CL,
198
+ QuickSwap Integral, symmetric Legacy Algebra and directional Legacy Algebra. V3 and Legacy
199
+ client discovery use `addresses`; Integral, V4 and Infinity use `references`, preserving
200
+ custom deployers and pool keys/IDs. V4 ID references still need indexed key lookup, with
201
+ per-request HTTP overrides supported; duplicate IDs share one lookup within the invocation, including compatibility fallback.
202
+ Infinity IDs resolve onchain within the Solidity reader.
94
203
 
95
- The StakingClient provides functionality for interacting with Steer Protocol staking pools. It supports both single and dual reward staking pools.
204
+ On supported providers, direct `amm.getPools()` uses one chain check plus one Solidity call
205
+ per page of at most 32 pools: **2 RPCs pinned for up to 32 pools**, 3 for 33 pools. Latest reads
206
+ add one head lookup. Readers inherit the existing single-pool identity checks and formatting;
207
+ only ephemeral helper code is overridden, never pool/token code, storage or balances.
96
208
 
97
- ### Features
209
+ Discovery has additional identity costs. On Base, a factory-based page using one AMM takes
210
+ 3 pinned RPCs; V4 manager discovery takes 2. Other chains can use direct factory reads when no
211
+ qualified Multicall deployment is configured. Each result retains its registered `amm`.
98
212
 
99
- - Fetch available staking pools
100
- - Stake and withdraw tokens
101
- - Claim rewards
102
- - Check earned rewards and balances
103
- - Calculate APR for staking pools
104
- - Support for both single and dual reward pools
213
+ The reader tries deployless execution, then helper-code overrides after explicit feature
214
+ rejection. If both are unsupported, the rest of that invocation uses compatibility reads
215
+ with at most four active pool loads. Custom `getPool()` overrides execute through that bounded
216
+ path and retain their normal behavior and additional RPC costs. Reverts, malformed responses,
217
+ identity errors, network failures and rate limits propagate without compatibility retries.
105
218
 
106
- ### Example Usage
219
+ The [batch-size benchmarks](benchmarks/README.md) explain the 32-pool/64-vault defaults.
220
+ Execution fallback and V4 key reuse live only for one invocation, including nested discovery
221
+ pages. Retained options and failed/concurrent calls cannot retain that state.
107
222
 
108
- ```typescript
109
- // Initialize the staking client
110
- const stakingClient = steerClient.staking;
223
+ ## Optional logging
111
224
 
112
- // Get all staking pools
113
- const pools = await stakingClient.getStakingPools();
225
+ Pass an application-owned logger when you want structured pool-listing events. Consola works
226
+ without a wrapper; install it in the application and use its numeric debug level:
114
227
 
115
- // Get pools for a specific chain
116
- const polygonPools = await stakingClient.getStakingPools(137); // Polygon chain ID
228
+ ```ts
229
+ import { createConsola } from "consola";
230
+ import { AmmProtocol, ChainId, SteerClient, UniswapVersion } from "@steerprotocol/sdk";
117
231
 
118
- // Stake tokens
119
- await stakingClient.stake({
120
- stakingPool: '0x...', // staking pool address
121
- amount: 1000000000000000000n, // amount in wei
232
+ const steer = new SteerClient({
233
+ logger: createConsola({ level: 4 }),
234
+ indexers: { theGraph: { proxyUrl: "/api/the-graph" } },
122
235
  });
236
+ const amm = steer.getAmm({ chainId: ChainId.Base, protocol: AmmProtocol.Uniswap, version: UniswapVersion.V3 });
123
237
 
124
- // Check earned rewards
125
- const earned = await stakingClient.earned('0x...', '0x...'); // pool address, account address
126
- console.log('Earned rewards:', earned.data);
127
-
128
- // Calculate APR
129
- const apr = stakingClient.calculateAPR(pool, rewardTokenPriceUSD, totalStakedUSD);
238
+ await amm.listPools({ token: { symbol: "USDC" } });
130
239
  ```
131
240
 
132
- ### Available Methods
241
+ The application still provides the proxy route. Node applications need an absolute proxy URL.
242
+ You can also pass any logger implementing the exported `Logger` interface. The SDK doesn't bundle
243
+ Consola, set logging levels, or configure reporters. Without a logger, it emits nothing.
244
+
245
+ Each `amm.listPools()` call gets an independent operation ID. Debug events describe query stages,
246
+ indexer provider/routing, and HTTP response headers. Each response includes a request ID, status,
247
+ and `headersDurationMs`. The info-level `amm.pools.list.completed` event includes `resultCount`
248
+ and total `durationMs`, including response body reading and parsing. A failure produces one
249
+ error-level `amm.pools.list.failed` event; caller cancellation produces a debug-level
250
+ `amm.pools.list.cancelled` event. Deadlines report `TIMEOUT` and caller cancellation reports
251
+ `CANCELLED`, including cancellation with a custom reason. An overlapping abort does not reclassify
252
+ an unrelated HTTP, network, or parsing failure. Public listing methods keep the two-argument
253
+ `listPools(request, options)` signature; operation context stays internal.
133
254
 
134
- - `getStakingPools(chainId?: number, protocol?: string)`: Fetch available staking pools
135
- - `stake(params: StakeParams)`: Stake tokens in a pool
136
- - `withdraw(params: WithdrawParams)`: Withdraw tokens from a pool
137
- - `getReward(params: GetRewardParams)`: Claim rewards
138
- - `earned(stakingPool: Address, account: Address)`: Get earned rewards
139
- - `totalSupply(stakingPool: Address)`: Get total staked tokens
140
- - `balanceOf(stakingPool: Address, account: Address)`: Get staked balance
141
- - `calculateAPR(pool: StakingPool, rewardTokenPriceUSD: number, totalStakedUSD: number)`: Calculate current pool APR
255
+ Events contain SDK-selected fields only. They exclude URLs, keys, headers, query bodies, token
256
+ filters, cursors, and raw error messages, stacks, and causes. Failures retain their original thrown
257
+ error for the caller. Synchronous logger errors and rejected logger promises don't affect SDK
258
+ results; the SDK doesn't wait for reporters or flush them. Events are shallow-frozen primitive
259
+ records. Reporters control their own formatting, filtering, storage, and delivery.
142
260
 
143
- ## Single-Asset Deposit
261
+ Logging currently covers the AMM pool-listing path through its data adapter, indexer, and HTTP
262
+ transport. Direct data-adapter calls, RPC reads/quotes, and vault operations don't emit operation
263
+ events yet.
144
264
 
145
- The Single-Asset Deposit feature allows users to deposit only one token (token0 or token1) into a liquidity vault, while the contract internally performs a swap to balance the token pair before depositing into the vault. This eliminates the need for users to provide both tokens in the correct ratio.
265
+ ## Import contracts directly
146
266
 
147
- ### Features
267
+ For contract metadata alone, use a named export from the package root:
268
+
269
+ ```ts
270
+ import { baseContracts } from "@steerprotocol/sdk";
271
+
272
+ baseContracts.VaultRegistry.address;
273
+ baseContracts.VaultRegistry.startBlock;
274
+ ```
148
275
 
149
- - **Single Token Input**: Deposit only token0 or token1 instead of both
150
- - **Automatic Balancing**: Internal swap to achieve the correct token ratio
151
- - **Slippage Protection**: Configurable slippage limits for both swap and deposit
152
- - **Preview Functionality**: Simulate the entire process before execution
153
- - **Modular Architecture**: Each step is separately testable and reusable
154
- - **Multiple AMM Support**: Currently supports UniswapV3, extensible for other AMMs
155
- - **Comprehensive Error Handling**: Detailed error messages and validation
276
+ Each chain has a deeply frozen contract map, such as `arbitrumContracts`, `polygonContracts`,
277
+ `bscContracts`, and `ethereumContracts`. `mainnetContracts` aliases `ethereumContracts`. These maps
278
+ are the source for the client catalog too; they don't construct a client or require an RPC transport.
156
279
 
157
- ### How It Works
280
+ ESM consumer bundlers can remove unused chains and client code. The Base-only browser fixture
281
+ currently measures about 1.34 kB minified / 554 bytes gzipped. `npm run check:bundle` verifies the built
282
+ package root import, excludes other-chain addresses, endpoints, and client code, and enforces a 3 kB
283
+ minified budget. It runs as part of `npm run check` after building. Sizes vary with bundler settings.
158
284
 
159
- The single-asset deposit process follows these steps:
285
+ `new SteerClient()` still includes the complete catalog so it can inventory and select all networks.
286
+ The savings above apply to bundled ESM consumers; a Node import or CommonJS `require` loads the
287
+ published module, and the npm installation still contains the complete package.
160
288
 
161
- 1. **Calculate Swap Amount**: Determine how much of the input token needs to be swapped
162
- 2. **Calculate Limit Price**: Apply slippage protection using current pool price
163
- 3. **Simulate Swap**: Preview the swap results without executing
164
- 4. **Estimate LP Tokens**: Calculate expected LP token output
165
- 5. **Execute Deposit**: Submit the transaction with all parameters
289
+ ## Chain inventory provenance
166
290
 
167
- ### Quick Start
291
+ `src/catalog/` and `src/contracts/` snapshot the effective values from the prior SDK's `src/const/network.ts` and
292
+ `src/const/subgraph.ts`, keyed by `src/const/chain.ts`. It includes all 455 contract entries after
293
+ network overrides and the FeeManager overlay, with numeric start blocks converted to bigint.
294
+ Missing optional fields remain absent. Tests compare every entry against the prior SDK directly;
295
+ the published package has no dependency on that implementation.
168
296
 
169
- ```typescript
170
- import { VaultClient, AMMType } from '@steerprotocol/sdk';
171
- import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
172
- import { mainnet } from 'viem/chains';
297
+ This inventory preserves legacy testnets and 32 archive subgraph URLs. Addresses and endpoints have
298
+ not been verified live. Catalog presence does not establish current deployment health, usable
299
+ indexing, AMM or lending support, or transaction feature support. No RPC URLs or credentials are
300
+ bundled.
173
301
 
174
- // Setup clients
175
- const publicClient = createPublicClient({
176
- chain: mainnet,
177
- transport: http(),
302
+ The old deployment files `goerli`, `optimismgoerli`, `arbitrumgoerli`, `mumbai`, `localhost`,
303
+ `bartiotest`, and `pyzkevm` were not registered networks in the prior SDK. They are not added to the
304
+ new catalog as additional chains.
305
+
306
+ ## Uniswap V3 on Base
307
+
308
+ ```ts
309
+ import { AmmFamily, ChainId, SteerClient, AmmProtocol, UniswapVersion } from "@steerprotocol/sdk";
310
+
311
+ const steer = new SteerClient({ transports: { [ChainId.Base]: rpcTransport } });
312
+ const pool = await steer.getPool({
313
+ chainId: ChainId.Base,
314
+ address: "0xd0b53D9277642d899DF5C87A3966A349A798F224",
315
+ family: AmmFamily.UniswapV3,
178
316
  });
179
317
 
180
- const walletClient = createWalletClient({
181
- chain: mainnet,
182
- transport: http(),
318
+ pool.amm.protocol;
319
+ pool.amm.family;
320
+ pool.amm.subgraphUrl;
321
+ pool.tick;
322
+ pool.liquidity;
323
+ pool.blockNumber;
324
+ pool.token0Decimals;
325
+ pool.token1Decimals;
326
+ pool.price?.currency0InCurrency1; // Whole token1 per whole token0, as a decimal string.
327
+ pool.price?.currency1InCurrency0; // Whole token0 per whole token1.
328
+ pool.fees.swapFeePercent; // "0.05" for raw fee 500.
329
+ pool.balances.token0.raw; // bigint from token0.balanceOf(pool.address).
330
+ pool.balances.token0.formatted; // Exact decimal string using token0Decimals.
331
+ pool.balances.token1.raw;
332
+ pool.balances.token1.formatted;
333
+
334
+ const amm = steer.getAmm({ chainId: ChainId.Base, protocol: AmmProtocol.Uniswap, version: UniswapVersion.V3 });
335
+ amm.contracts.factory.address;
336
+ amm.subgraphUrl;
337
+ const state = await amm.getPool(pool.address, { blockNumber: pool.blockNumber });
338
+ // Also exposes token0, token1, fees.swapFeePips, tickSpacing, factory, and sqrtPriceX96.
339
+ const quote = await amm.quote({
340
+ pool: pool.address,
341
+ inputCurrency: state.token0,
342
+ outputCurrency: state.token1,
343
+ amountIn: 1_000_000_000_000_000n,
344
+ }, { blockNumber: state.blockNumber });
345
+ ```
346
+
347
+ `rpcTransport` implements `request(method, params): Promise<unknown>` and returns the JSON-RPC
348
+ `result`, throwing RPC errors. The SDK verifies `eth_chainId`, chooses one block number (or accepts
349
+ an explicit `blockNumber`, including zero), and pins the operation's contract reads to that block.
350
+ This pins the block number; it does not guarantee finality or protect against a block reorganization.
351
+
352
+ Pool reads include ERC-20 `decimals()` and `balanceOf(pool.address)` calls for both tokens at that
353
+ same block. Missing or invalid
354
+ decimals fail the read; the SDK never assumes 18. The token addresses and raw `sqrtPriceX96`, `fees.swapFeePips`,
355
+ `tick`, `tickSpacing`, and `liquidity` remain available.
356
+
357
+ `price` contains decimal-adjusted spot prices in both directions, computed with bigint arithmetic
358
+ and formatted with up to 36 significant digits, rounded down. Strings have no scientific notation or
359
+ unnecessary trailing fractional zeros; each direction derives from the raw ratio independently.
360
+ Very small nonzero prices remain nonzero. An uninitialized pool (`sqrtPriceX96 === 0n`) returns
361
+ `price: undefined`. `fees.swapFeePercent` is an exact percentage string without a percent sign.
362
+
363
+ The calculation follows [Uniswap's V3 math primer](https://blog.uniswap.org/uniswap-v3-math-primer):
364
+ `currency0InCurrency1 = sqrtPriceX96² / 2¹⁹² × 10^(token0Decimals - token1Decimals)`.
365
+ These are pool spot prices. Liquidity stays in its raw mathematical units; it isn't a token balance
366
+ or TVL, and spot prices don't include a trade's fees or price impact.
367
+
368
+ `balances.token0` and `balances.token1` each contain the raw bigint and an exact formatted decimal
369
+ string, without rounding or scientific notation. These are the tokens held by the pool contract,
370
+ including any fees or donated tokens. They don't convert `liquidity`, calculate a particular LP's
371
+ position amounts, or assign fee ownership. Balance reads use the same pinned block as the rest of
372
+ the snapshot. If either read fails, the SDK propagates the error instead of returning partial or
373
+ invented zero balances. The snapshot and its nested balance objects are frozen.
374
+
375
+ Family discovery matches the pool's factory address to exactly one registered deployment. The
376
+ adapter additionally checks the factory's `getPool(token0, token1, fee)` result, rejecting an address
377
+ that impersonates a registered pool. Unknown or ambiguous mappings fail without a default protocol.
378
+ Reads and quotes propagate RPC/decode errors, and no method signs or submits transactions.
379
+
380
+ Reusable V3 reads and QuoterV2 behavior live in `RpcUniswapV3Adapter`; `UniswapV3` supplies protocol
381
+ identity. Other compatible protocols can subclass that implementation and register their own
382
+ contracts and data adapter. Current family-based discovery supports only V3 address references;
383
+ V4 discovery is not implemented. The deployment uses the addresses in the
384
+ [official Base deployment list](https://developers.uniswap.org/docs/protocols/v3/deployments/v3-base-deployments).
385
+
386
+ The Uniswap subgraph URL retains the prior SDK's subgraph ID in a gateway URL without credentials.
387
+ Gateway queries require consumer authentication. Pool reads and quotes use RPC independently of
388
+ indexed listing.
389
+
390
+ ### List indexed pools
391
+
392
+ Configure The Graph once on the client, using an API key in server code:
393
+
394
+ ```ts
395
+ const steer = new SteerClient({
396
+ indexers: { theGraph: { apiKey: theGraphApiKey } },
183
397
  });
398
+ const amm = steer.getAmm({ chainId: ChainId.Base, protocol: AmmProtocol.Uniswap, version: UniswapVersion.V3 });
399
+ const first = await amm.listPools({ limit: 100, token: { symbol: "USDC" } });
184
400
 
185
- // Initialize vault client
186
- const vaultClient = new VaultClient(publicClient, walletClient, 'production');
187
-
188
- // Preview single-asset deposit
189
- const preview = await vaultClient.previewSingleAssetDeposit(
190
- {
191
- assets: parseEther('100'), // 100 tokens to deposit
192
- receiver: userAddress, // Address to receive LP tokens
193
- vault: vaultAddress, // Vault contract address
194
- isToken0: true, // Depositing token0
195
- depositSlippagePercent: 5n, // 5% max slippage for deposit
196
- swapSlippageBP: 500, // 5% slippage for internal swap (in basis points)
197
- ammType: AMMType.UniswapV3, // AMM type
198
- singleAssetDepositContract: contractAddress, // Single-asset deposit contract
199
- },
200
- poolAddress
201
- );
401
+ first.items; // Pool addresses; load one with amm.getPool(address) for verified RPC state.
202
402
 
203
- if (preview.success) {
204
- console.log('Expected LP tokens:', preview.data.lpEstimation.lpTokens);
205
- console.log('Amount to be swapped:', preview.data.swapAmount);
206
- console.log('Final token0 amount:', preview.data.lpEstimation.finalAmount0);
207
- console.log('Final token1 amount:', preview.data.lpEstimation.finalAmount1);
208
-
209
- // Execute the deposit
210
- const result = await vaultClient.singleAssetDeposit({
211
- assets: parseEther('100'),
212
- receiver: userAddress,
213
- vault: vaultAddress,
214
- isToken0: true,
215
- depositSlippagePercent: 5n,
216
- swapSlippageBP: 500,
217
- ammType: AMMType.UniswapV3,
218
- singleAssetDepositContract: contractAddress,
219
- });
403
+ await amm.listPools({ token: { address: tokenAddress } });
404
+ await amm.listPools({ token: { name: "USD Coin" } });
220
405
 
221
- if (result.success) {
222
- console.log('Transaction hash:', result.data);
223
- }
406
+ if (first.nextCursor) {
407
+ const next = await amm.listPools({ limit: 100, cursor: first.nextCursor });
224
408
  }
409
+
410
+ amm.dataAdapter; // UniswapV3DataAdapter behind the listing operation.
411
+ amm.dataAdapter.subgraphUrl;
225
412
  ```
226
413
 
227
- ## Block-pinned vault execution context
414
+ For browser applications, configure a server proxy instead of putting a private API key in the client:
228
415
 
229
- Strategy execution consumers can read a normalized, JSON-safe vault context without selecting
230
- protocol adapters or assembling pool state themselves:
416
+ ```ts
417
+ const steer = new SteerClient({
418
+ indexers: { theGraph: { proxyUrl: "/api/the-graph" } },
419
+ });
420
+ const amm = steer.getAmm({ chainId: ChainId.Base, protocol: AmmProtocol.Uniswap, version: UniswapVersion.V3 });
421
+ const pools = await amm.listPools({ token: { symbol: "USDC" } });
422
+ ```
423
+
424
+ Choose either `apiKey` or `proxyUrl`. A proxy receives a POST to
425
+ `/api/the-graph?subgraphId=<id>` with the original GraphQL query and variables. Existing URL query
426
+ parameters are preserved. Your application implements that route, validates the intended subgraph,
427
+ and forwards the request using its server-side key. Use an absolute HTTP(S) proxy URL in Node;
428
+ browser applications can use an absolute URL or a path starting with `/`.
231
429
 
232
- ```typescript
233
- import { getVaultExecutionContext } from '@steerprotocol/sdk';
430
+ Configuration is snapshotted per client. The SDK applies the key or proxy only to recognized The
431
+ Graph gateway subgraph URLs. Other providers and RPC operations are unaffected. `subgraphUrl` remains
432
+ the canonical indexer URL; API keys and proxy settings never become public deployment metadata.
234
433
 
235
- const context = await getVaultExecutionContext({
236
- publicClient,
237
- vaultAddress: '0x666805942995ff8494294720a1dadb7cd2348750',
238
- blockNumber: 49_251_269n,
239
- epochTimestamp: 1_800_000_000n, // Unix seconds; explicit, never wall-clock time
434
+ Per-request overrides remain available:
435
+
436
+ ```ts
437
+ await amm.listPools({ token: { symbol: "USDC" } }, {
438
+ headers: { Authorization: `Bearer ${otherApiKey}` },
439
+ timeoutMs: 5_000,
240
440
  });
441
+ ```
442
+
443
+ An explicit authorization header overrides the client key, using case-insensitive header matching.
444
+ Other request headers are preserved. A custom `fetch` overrides the HTTP implementation while still
445
+ receiving the configured destination and headers, including client authentication in API-key mode.
446
+ Proxy mode does not add an SDK API-key header. Optional `SteerClient({ http: { headers, fetch,
447
+ timeoutMs, signal } })` defaults apply to indexed HTTP requests and are snapshotted per client.
448
+ Per-request settings override those defaults; headers merge case-insensitively. RPC transports remain
449
+ independent. Adapter-defined headers (such as JSON content type) override client defaults, while
450
+ explicit per-request headers take precedence over both.
451
+
452
+ HTTP requests default to a 15-second timeout. An explicit cancellation signal and the timeout both
453
+ remain active; `timeoutMs: 0` disables the timeout. Per-request signals override a client default
454
+ signal. HTTP failures throw the root-exported `HttpError` with a numeric `status`; failed response bodies
455
+ are cancelled without replacing that error if cleanup fails. Network, timeout, and cancellation
456
+ errors propagate.
457
+ Client construction never makes requests.
458
+
459
+ The internal native-fetch transport handles HTTP settings, request sending, deadlines, cancellation,
460
+ and HTTP status failures, returning the response without parsing it. The indexer layer selects the
461
+ destination and provider authentication. Data adapters serialize GraphQL queries, parse response
462
+ bodies and GraphQL errors, and own filtering and pagination. No additional HTTP dependency is used.
463
+
464
+ A token filter matches either token in the pool. Addresses match exactly (ignoring address case);
465
+ symbols and names use case-insensitive substring matching. Multiple supplied fields must match the
466
+ same token. Blank filters and empty search text are rejected. Symbols and names are indexed metadata,
467
+ so they can match multiple unrelated token contracts; use an address to select a particular token.
468
+
469
+ Listing returns up to 1000 addresses per page (100 by default), ordered by address. The opaque cursor
470
+ retains the endpoint, factory, token filter, subgraph deployment, and indexed block number from the first page.
471
+ Later pages use `id_gt` at that block, without offset pagination. Token filters are restored from the
472
+ cursor when omitted; supplying different filters with an existing cursor is rejected. Treat cursors as opaque and restart
473
+ listing if the subgraph deployment changes. A full final page can produce a cursor whose next page
474
+ is empty. Indexed blocks remain subject to chain reorganizations and indexer availability.
475
+
476
+ The adapter checks the indexed factory and rejects indexing errors, partial GraphQL errors, HTTP
477
+ failures, malformed data, and repeated or unsorted addresses. Indexed results remain discovery data;
478
+ `getPool` performs the independent RPC ownership checks. Lending and Steer listing implementations
479
+ remain outside this change.
480
+
481
+ The query follows the [Uniswap V3 schema](https://github.com/Uniswap/v3-subgraph/blob/main/src/v3/schema.graphql)
482
+ and [The Graph pagination and historical-query API](https://thegraph.com/docs/en/subgraphs/querying/graphql-api/).
483
+ Bearer authentication follows [The Graph API-key documentation](https://thegraph.com/docs/en/subgraphs/providers/subgraph-studio/managing-api-keys/).
484
+
485
+ ### RPC request counts
486
+
487
+ Successful Uniswap V3 operations currently make these calls through the configured `RpcTransport`:
488
+
489
+ | Operation | Select latest block | Explicit block |
490
+ | --- | ---: | ---: |
491
+ | `amm.getPool()` | 3 | 2 |
492
+ | `steer.getPool()` with discovery | 4 | 3 |
493
+ | `amm.quote()` | 6 | 5 |
494
+
495
+ Discovery reads the factory, selects an exact registered adapter, and dispatches to that adapter's
496
+ `getPool()` method before any snapshot reads. Custom adapters can handle nonstandard metadata
497
+ through their own read path. The standard adapter then loads and verifies its snapshot. This costs
498
+ one more RPC request than a direct adapter read. Chain, block, and discovered factory context are
499
+ shared internally; no snapshot is preloaded or passed through discovery. Each operation validates
500
+ the RPC chain, with no cross-operation identity cache or additional consumer options.
501
+
502
+ Quotes read pool identity, the token pair and fee, verify factory registration, and call QuoterV2.
503
+ They don't load token decimals, balances, ticks, or liquidity. Discovering a pool and then quoting
504
+ at its block uses 9 RPC calls in total. Counts exclude retries and provider-specific transport
505
+ behavior.
506
+
507
+ Pool snapshots use a small internal Solidity reader to follow all dependencies within one
508
+ `eth_call`. The reader checks the pool's factory against the configured allowlist and verifies
509
+ factory registration before reading token metadata or balances. JavaScript keeps decimal formatting
510
+ and returns the same public snapshot shape. No contract is deployed, no transaction is submitted,
511
+ and no consumer configuration changes are needed.
512
+
513
+ The SDK prefers viem's deployless bytecode execution. If the provider explicitly rejects that
514
+ feature, it tries a temporary code override at an internal helper address. The override supplies
515
+ helper code only; pool and token code, storage, and balances remain untouched. Both mechanisms have
516
+ been verified against the configured Base RPC and produce identical same-block snapshots.
517
+
518
+ If both execution features are explicitly rejected, pool reads retain viem's native Multicall3
519
+ path: one aggregate for pool state and another for metadata/balances, after direct factory checks.
520
+ The two rejected feature attempts add two requests to this compatibility path: 8 total for latest,
521
+ or 7 for an explicit Base block with Multicall3. A provider supporting only overrides uses 4 or 3
522
+ for direct pool reads, and 5 or 4 for discovery.
523
+ There is no global provider capability or identity cache. Reverts, identity failures, malformed
524
+ responses, rate limits, and network errors fail without silently choosing another execution path.
525
+ Structured revert evidence anywhere in the error chain takes precedence over feature-like wording;
526
+ explicit unsupported-feature messages can put the feature name before or after the rejection.
527
+
528
+ Quotes continue using Multicall3 for the token pair and fee, with factory checks and QuoterV2 calls
529
+ kept direct. Every reader, aggregate, and direct call uses the operation's explicit block and the
530
+ same injected `RpcTransport`. Required member failures reject the operation without partial values.
531
+
532
+ Base Multicall3 metadata matches viem's deployment at block 5022. Where Multicall3 is unavailable,
533
+ the compatibility path uses direct reads instead: pool reads/discovery use 14 calls for latest or
534
+ 13 for an explicit block, plus the two rejected reader attempts; quotes use 8 or 7. HTTP JSON-RPC
535
+ batching remains a separate transport concern.
536
+
537
+ ### Solidity reader development
538
+
539
+ The sources are `src/amm/adapters/uniswap-v3/reader/V3PoolReader.sol` and `src/vault/adapters/uniswap-v3/reader/V3VaultReader.sol`; their internal ABIs and
540
+ creation/runtime bytecode are checked in under `src/amm/adapters/uniswap-v3/reader/artifact.ts` and
541
+ `src/vault/adapters/uniswap-v3/reader/artifact.ts`. `npm run build:reader` regenerates both artifacts with
542
+ the pinned development-only Solidity compiler, optimizer settings, and Paris EVM target.
543
+ `npm run check:reader`, included in `npm run check`, detects stale artifacts. Consumers receive
544
+ compiled JavaScript and bytecode and do not need Solidity tools.
545
+
546
+ Offline tests execute the real reader bytecode in a development-only EVM with mock pools,
547
+ factories, and tokens. Live tests exercise deployless execution, code overrides, and the Multicall
548
+ compatibility path against the configured RPC and compare with independent same-block reads.
549
+
550
+ ## Register integrations
551
+
552
+ Implement a family adapter and its data adapter, then define an internal `Deployment` object whose factory
553
+ constructs that adapter. The factory receives the chain context and deployment metadata.
554
+ SDK catalog entries bind a network's own Steer deployment to its AMM and lending integrations.
555
+ SDK integration code and tests can supply `new SteerClient({ catalog, transports })` to override the
556
+ catalog. `ChainDefinition` contains network identity and transaction/wrapped-token metadata. `SteerNetwork` is a
557
+ connected Steer deployment with one resolver-backed loading path. Steer deployment factories create
558
+ data adapters, so they don't need to implement unused vault loaders. Constructing a client must not
559
+ perform network calls.
560
+
561
+ `tests/fixtures/composition.ts` contains local fakes that exercise the extension contracts; don't
562
+ use their addresses or responses in an app.
563
+
564
+ AMM consumers select the protocol brand and its version with enums. Versions use public product
565
+ names: QuickSwap V3 selects Algebra Legacy, while QuickSwap V4 selects Algebra Integral. Family
566
+ selection comes from explicit adapter/deployment configuration, never a name substring or ABI probe.
241
567
 
242
- if (!context.success) throw new Error(context.error);
243
-
244
- console.log(context.data.protocol, context.data.currentTick);
245
- console.log(context.data.totalAmount0, context.data.lastExecutionTimeSeconds);
246
- ```
247
-
248
- The same capability is available as `vaultClient.getExecutionContext(...)`. The SDK resolves the
249
- vault's beacon name, beacon address, and protocol through the deployed `VaultRegistry`, verifies
250
- non-v4 pool factories against SDK protocol configuration, and reads positions, totals, tick spacing,
251
- and pool state at the exact supplied block. Uniswap v4 uses the vault's complete `poolKey()`, derives
252
- its pool ID, and reads state through the configured `StateView`; an optional `poolKey` input acts as
253
- an assertion and must match exactly.
254
-
255
- All integer values that may exceed JavaScript's safe range are returned as decimal strings. The
256
- `provenance` object identifies block-pinned contract data. Last execution comes from the established
257
- Steer `vaultSnapshots` entity, queried below both the explicit epoch and pinned-block timestamp; its
258
- provenance is marked `timestamp-bounded` because the entity does not expose a block number. When no
259
- earlier snapshot exists, both last-execution fields are `null`; lookup or historical consistency
260
- failures return an unsuccessful `SteerResponse` rather than falling back to latest state.
261
-
262
- ### Advanced Usage
263
-
264
- #### Using Individual Functions
265
-
266
- Each step can be used independently for testing or custom implementations:
267
-
268
- ```typescript
269
- import {
270
- calculateSwapAmount,
271
- calculateLimitPrice,
272
- simulateSwap,
273
- estimateLpTokens,
274
- AMMType,
275
- } from '@steerprotocol/sdk';
276
-
277
- // Step 1: Calculate swap amount
278
- const swapResult = await calculateSwapAmount(publicClient, {
279
- depositAmount: parseEther('100'),
280
- isToken0: true,
281
- vault: vaultAddress,
282
- pool: poolAddress,
283
- ammType: AMMType.UniswapV3,
284
- singleAssetDepositContract: contractAddress,
568
+ ```ts
569
+ const polygon = steer.getAmm({
570
+ chainId: ChainId.Polygon,
571
+ protocol: AmmProtocol.QuickSwap,
572
+ version: QuickSwapVersion.V3,
285
573
  });
574
+ const base = steer.getAmm({
575
+ chainId: ChainId.Base,
576
+ protocol: AmmProtocol.QuickSwap,
577
+ version: QuickSwapVersion.V4,
578
+ });
579
+ ```
286
580
 
287
- // Step 2: Calculate price limit for slippage protection
288
- const limitPrice = await calculateLimitPrice(publicClient, {
289
- pool: poolAddress,
290
- slippageBP: 500,
291
- zeroForOne: true,
292
- ammType: AMMType.UniswapV3,
581
+ `UniswapVersion` offers V3/V4, `QuickSwapVersion` offers V3/V4, `CamelotVersion` offers V3, and
582
+ `PancakeSwapVersion` offers V3/InfinityCL. These enumerate implemented adapters, not every product
583
+ published by each brand. Only qualified chain deployments are registered.
584
+
585
+ Lookup infers the concrete adapter type from protocol and version, including its direct methods
586
+ and `contracts` accessor. Omitting version is allowed only when the remaining selectors identify
587
+ one deployment; its static return type includes the protocol's supported versions. Base Uniswap
588
+ and BNB PancakeSwap require a version because each has two registered versions. Ambiguity errors
589
+ list the available versions and deployment IDs. There is no implicit latest version.
590
+
591
+ When multiple deployments of the same version exist, use the optional named selector:
592
+
593
+ ```ts
594
+ const amm = steer.getAmm({
595
+ chainId: ChainId.Base,
596
+ protocol: AmmProtocol.Uniswap,
597
+ version: UniswapVersion.V3,
598
+ deployment: AmmDeployment.UniswapV3Base,
293
599
  });
600
+ ```
601
+
602
+ Named selectors do not bypass chain, protocol, version or family validation. Each adapter exposes
603
+ `amm.protocol`, `amm.version`, and `amm.family` separately. Pool discovery and vault resolution
604
+ continue to select adapters by exact factory/manager or beacon/implementation mappings; consumers
605
+ need not supply a version to those resolution flows. AMM deployment objects stay internal and are not root exports. Registries reject
606
+ cross-chain deployments, duplicate IDs, and unregistered objects. Each lookup constructs a fresh
607
+ adapter; there is no global registry or shared client cache. Use `amm.contracts.factory.address` and `amm.subgraphUrl` for direct metadata access.
608
+
609
+ Vault implementation files live under `src/vault/adapters/uniswap-v3/`: `vault.ts` owns vault
610
+ operations; `deployment.ts` owns exact Base resolver/underlying mappings; `identity.ts` holds pinned
611
+ code identities; `rpc.ts` and `reader/` hold internal reads. Shared models and enums live in
612
+ `vault/types.ts` and `vault/enums.ts`. The Steer data-adapter scaffold lives in
613
+ `vault/data-adapter.ts`; built-in indexed listing supports optional AMM/version and token filters.
614
+
615
+ ## Automatic vault resolution
616
+
617
+ `getVault(chainId, address, { blockNumber })` uses the network's SDK-owned `vaultResolver` configuration. Consumers
618
+ don't select a vault family or pass an AMM adapter. Resolution selects a block, reads the registered
619
+ vault identity, and matches an exact beacon-address and implementation-address pair. The resolver
620
+ checks the requested chain, vault and registry addresses, and agreement between the registry's
621
+ beacon and the proxy's actual beacon.
622
+
623
+ Each implementation binding supplies an ABI-specific `readUnderlying` function and vault
624
+ constructor. The reader reports configured pool or market references and their factory, PoolManager,
625
+ or lending contract addresses. It doesn't require active positions, balances, or an existing deposit.
626
+ Exact address and contract-role bindings select registered adapters. The constructor must retain the
627
+ resolved references in `vault.underlying`, including every integration used by a composite vault.
628
+ A liquidity or lending vault also retains its corresponding adapter as `amm` or `lending`.
629
+
630
+ The specialized getters call this same resolver and reject a result of the wrong kind. Unknown
631
+ implementations, unknown underlying contracts, duplicate mappings, and mismatched identities produce
632
+ `VaultResolutionError` with an enum-backed code. RPC errors propagate. No loader uses substring
633
+ matching or guesses a default family. A network without resolver configuration reports
634
+ `NotConfigured` rather than falling back to another loader.
635
+
636
+ Identity and configured-reference reads receive the same block number. Every load reads identity again, so an
637
+ upgrade must match a supported implementation binding before the SDK constructs a new vault.
638
+ Adapters are reused within a single resolution, without retaining an identity cache across loads.
639
+
640
+ Base includes an onchain reader and an exact mapping for the `MultiPositionUniswapV3` beacon
641
+ `0xDf0b9Ef4d7C4E294A74efB4407129bd66e4f82f0` and implementation
642
+ `0xBDDe75BBf4a580c5E564B5f9aC7B12007dD307C6`. It verifies registry membership, the actual
643
+ proxy beacon slot, proxy/implementation runtime hashes, and the vault's stored registry. It also
644
+ verifies the underlying factory, factory registration, and token ordering. Indexed metadata alone
645
+ isn't enough. Unsupported upgrades fail explicitly. Other vault implementations remain unmapped.
646
+
647
+ ### Polygon liquidity vaults
648
+
649
+ Use the same automatic resolver and snapshot API. Consumers do not select a vault family:
294
650
 
295
- // Step 3: Simulate the swap
296
- const simulation = await simulateSwap(
297
- publicClient,
298
- {
299
- pool: poolAddress,
300
- recipient: userAddress,
301
- zeroForOne: true,
302
- amountSpecified: parseEther('10'),
303
- sqrtPriceLimitX96: limitPrice,
304
- ammType: AMMType.UniswapV3,
305
- tokenIn: '0x...', // Token being swapped from
306
- tokenOut: '0x...', // Token being swapped to
307
- fee: 3000, // Pool fee in basis points
308
- },
309
- 137
310
- ); // Chain ID (e.g., 137 for Polygon)
311
-
312
- // Step 4: Estimate LP tokens
313
- const estimation = await estimateLpTokens(publicClient, {
314
- vault: vaultAddress,
315
- originalAssets: parseEther('100'),
316
- swapAmount: swapResult.data.swapAmount,
317
- swapResult: simulation.data,
318
- isToken0: true,
651
+ ```ts
652
+ const vault = await steer.getLiquidityVault(ChainId.Polygon, vaultAddress);
653
+ const snapshot = await vault.getSnapshot();
654
+ const snapshots = await steer.getVaultSnapshots(ChainId.Polygon, vaultAddresses);
655
+
656
+ const page = await steer.listVaults({
657
+ chainId: ChainId.Polygon,
658
+ amm: { protocol: AmmProtocol.QuickSwap, version: QuickSwapVersion.V3 },
659
+ token: { symbol: "USDC" },
319
660
  });
320
661
  ```
321
662
 
322
- #### Using the SingleAssetDepositClient Directly
663
+ Polygon mappings cover these exact beacon deployments:
664
+
665
+ | Indexed beacon | Underlying AMM | Accounting policy |
666
+ | --- | --- | --- |
667
+ | `MultiPositionUniswapV3` | Uniswap V3 | Vault `TOTAL_FEE` and accrued Steer/strategist reserves |
668
+ | `MultiPositionSushi` | SushiSwap V3 | Same fixed-fee V3 layout |
669
+ | `MultiPositionRetro` | Retro V3 | Same fixed-fee V3 layout |
670
+ | `MultiPositionQuickSwap` | QuickSwap V3 / Legacy Algebra | Fixed fees; packed position key and six-field pool position ABI |
671
+ | `MultiPositionQuickSwapIntegral` | QuickSwap V4 / Algebra Integral | Existing Integral fee-manager reader |
672
+
673
+ The SDK verifies actual registry membership, stored beacon/registry, exact implementation and original
674
+ proxy/implementation runtime hashes, factory registration and token ordering. Indexed names only filter
675
+ discovery. V3 readers reuse the existing pool/position math with a fixed-fee policy subclass; Integral
676
+ reuses its existing reader. Legacy keeps its pool ABI, reader and compatibility path in its adapter folder.
677
+ SushiSwap and Retro retain their own typed protocol identities while sharing the V3 family. Their
678
+ optional AMM listing endpoints retain prior SDK metadata and have not been qualified by the vault benchmark;
679
+ no quoter is configured for those deployments.
680
+
681
+ Polygon also qualifies the newer Solidity 0.8.26 proxy by its exact runtime hash and supports the
682
+ historical QuickSwap beacon `0x50cb5e8ae49a7457e2a15acb429b86c86f2f8fc2`. Historical resolution
683
+ checks the actual stored beacon, exact implementation/hash, registry membership, current name mapping
684
+ and the registry's reverse name for the old beacon. Unknown histories still fail. See the
685
+ [identity qualification](benchmarks/polygon-vault-identity-qualification-2026-10-19.json).
686
+
687
+ The historical implementation deducts fees after aggregating positions. Its position net fees allocate
688
+ rounding carry cumulatively so their sum matches the snapshot and original vault accounting. These
689
+ allocations describe snapshot accounting, not independent position withdrawal entitlements. Current
690
+ implementations retain their existing per-position rounding.
691
+
692
+ Both QuickSwap versions are now registered on Polygon, so `getAmm` and AMM-filtered `listVaults`
693
+ require a version (or an exact deployment selector). Neither chooses an implicit latest version.
694
+ Unfiltered vault listing remains available without RPC configuration.
695
+
696
+ Bulk snapshots retain the 16-vault / 128-position budgets, same-block accounting, registered adapter
697
+ and vault overrides, and strict compatibility fallback. Scheduled/dynamic jobs, single-position vaults,
698
+ test implementations, unknown proxy runtimes and conflicting registry/proxy identities remain explicit
699
+ failures. A bulk call is atomic; it does not silently omit rejected addresses. See the
700
+ [deployment qualification](benchmarks/polygon-vault-deployments-2026-10-19.json) and
701
+ [full-list benchmark](benchmarks/polygon-vault-snapshots.md) for measured coverage and timing.
702
+
703
+ ### Read a Base V3 vault
323
704
 
324
- ```typescript
325
- import { SingleAssetDepositClient } from '@steerprotocol/sdk';
705
+ ```ts
706
+ import { ChainId, SteerClient } from "@steerprotocol/sdk";
326
707
 
327
- const client = new SingleAssetDepositClient(publicClient, walletClient);
708
+ const steer = new SteerClient({ transports: { [ChainId.Base]: rpcTransport } });
709
+ // Same automatic resolution as getVault(), with a liquidity-kind assertion.
710
+ const vault = await steer.getLiquidityVault(
711
+ ChainId.Base,
712
+ "0x022480ee800f4aad2d12099d5a87e4d1c16a975f",
713
+ );
714
+ const snapshot = await vault.getSnapshot();
715
+
716
+ vault.amm.protocol;
717
+ vault.amm.subgraphUrl;
718
+ vault.steer.subgraphUrl;
719
+ snapshot.blockNumber;
720
+ snapshot.pool;
721
+ snapshot.idle.token0.formatted; // Available idle tokens after fee reserves.
722
+ snapshot.balances.token0.raw; // Full vault ERC-20 holdings.
723
+ snapshot.reservedFees.token0.raw; // Already collected protocol/strategist fees.
724
+ snapshot.principal.token0.formatted; // Sum of deployed position principal.
725
+ snapshot.fees.gross.token0.formatted; // All uncollected position fees.
726
+ snapshot.fees.net.token0.formatted; // LP portion after the vault fee cut.
727
+ snapshot.fees.recorded.token0.raw; // Sum of pool-checkpointed fees across positions.
728
+ snapshot.feeRateBps;
729
+ snapshot.total.token0.formatted; // idle + principal + net fees.
730
+ // Every amount pair also includes token1. Raw amounts are bigint; formatted amounts are strings.
731
+
732
+ for (const position of snapshot.positions) {
733
+ position.positionId; // keccak256(vault address, lowerTick, upperTick).
734
+ position.lowerTick;
735
+ position.upperTick;
736
+ position.relativeWeight; // Contract-configured units, not basis points.
737
+ position.weightPercent; // Normalized percentage string, not a value allocation.
738
+ position.liquidity;
739
+ position.principal.token0.formatted;
740
+ position.fees.gross.token0.formatted; // Gross collectible fees, including accrued growth.
741
+ position.fees.net.token0.formatted;
742
+ position.fees.recorded.token0.raw; // Pool checkpoint tokensOwed, before new fee growth.
743
+ }
328
744
 
329
- // Preview deposit
330
- const preview = await client.previewDeposit(params, poolAddress);
745
+ const positions = await vault.getPositions({ blockNumber: snapshot.blockNumber });
746
+ const shares = await vault.getShareBalance(accountAddress, { blockNumber: snapshot.blockNumber });
747
+ ```
331
748
 
332
- // Prepare transaction (without executing)
333
- const preparedTx = await client.prepareDepositTx(params);
749
+ Omit `blockNumber` to choose the current RPC block, or pin it on resolution and each read. Each
750
+ snapshot pins identity, ranges, fee configuration, token metadata, balances and pool state to one
751
+ block number. Each read revalidates identity; an existing object cannot silently use an upgraded
752
+ implementation or a changed pool. Pool state goes through the registered adapter's `getPool()`
753
+ method, including overrides. Vault reads need RPC only, without a Graph API key.
754
+
755
+ Snapshot reads prefer the internal `V3VaultAddressReader` through a code-only override at the
756
+ vault address. First, `eth_getCode` verifies the original proxy runtime at the selected block.
757
+ The overridden reader checks `block.chainid`, reads the actual beacon/registry/fee-manager/helper
758
+ slots with SLOAD, and verifies registry membership, exact beacon/implementation mappings and
759
+ unchanged implementation/helper runtime hashes. Original vault view selectors are forwarded to the
760
+ verified implementation through delegatecall, retaining the vault's address and storage context.
761
+ Pool registration, token ordering, balances, ranges and fee-growth reads remain independent of
762
+ adapter overrides. No storage, balances, pool/token code or implementation code is overridden.
763
+ Everything runs in eth_call; deployed code and state are unchanged.
764
+
765
+ A pinned snapshot uses **2 RPC requests**: original proxy code plus one overridden snapshot.
766
+ Latest adds one block-number request. These counts exclude resolution, which now uses **2**
767
+ pinned requests; a cold pinned resolve plus snapshot therefore uses **4** requests. Both the
768
+ V3 address reader and the existing helper reader feed the same TypeScript accounting code.
769
+
770
+ Only explicit unsupported-override responses activate the existing helper reader. That fallback
771
+ uses four same-block storage reads, then deployless execution, then helper-address code override,
772
+ and finally Multicall compatibility after explicit feature rejections. Including the new proxy
773
+ preflight and rejected address-override probe, those pinned snapshot paths use **8**, **9**, and
774
+ **27** requests respectively for one or two ranges; ten ranges add one compatibility batch.
775
+ Reverts, malformed results, identity/accounting failures, network errors and rate limits propagate.
776
+ There is no cross-call provider capability cache and no fallback after an actual reader failure.
777
+
778
+ Every path dispatches the registered adapter's `getPool()` override. The default adapter reuses
779
+ an immutable copy of verified raw pool data only within that invocation, bound to chain, transport,
780
+ pool and block. The returned adapter identity is checked against the independently verified reader
781
+ result. Retained options expire on success and failure; concurrent invocations remain isolated.
782
+ Internal raw data and execution modes are absent from public options and declarations.
783
+
784
+ `getPositions()` returns configured LP ranges, including zero-liquidity ranges; idle tokens live in
785
+ `getSnapshot().idle`. Weights come directly from the vault and describe its configured distribution,
786
+ not current dollar-value allocations. Percentages round down to 18 decimal places and are undefined
787
+ when all weights are zero. Duplicate ranges fail explicitly to prevent counting one pool position
788
+ twice. An empty position array still retains `vault.underlying` and `vault.amm`.
789
+
790
+ V3 principal and fee-growth calculations use bigint math, exact tick ratios and contract rounding.
791
+ Position and snapshot `fees` both use `{ gross, net, recorded }`; snapshot values sum the
792
+ corresponding position amounts. `feeRateBps` is a vault-level field on the snapshot. Recorded fees
793
+ are already included in gross, so they must not be added again. Gross fees include both recorded
794
+ `tokensOwed` and fees accrued since the last pool checkpoint,
795
+ including uint256 fee-growth wraparound. Net fees apply the vault's fee-manager rate per position,
796
+ rounded down, matching its verified helper. The reader uses the vault's stored fee manager and
797
+ helper for this exact implementation, verifies helper code, and rejects invalid rates or reserves
798
+ that exceed token holdings. It never substitutes the registry's current fee manager for the vault's.
799
+ `total` is an accounting snapshot, not a withdrawal quote. Use `previewWithdraw()` for the
800
+ contract-calculated exit amounts. Deposit quotes use original post-poke totals and the qualified share formula.
801
+
802
+ The storage layout and accounting are verified against exact-match Sourcify sources for the
803
+ [Base vault implementation](https://sourcify.dev/server/v2/contract/8453/0xBDDe75BBf4a580c5E564B5f9aC7B12007dD307C6?fields=all)
804
+ and its [helper](https://sourcify.dev/server/v2/contract/8453/0xa98376572375692455172f6750c67339667974fe?fields=all).
805
+ Offline tests retain public runtime bytecode in `tests/fixtures/v3-vault-code.json` to check these
806
+ hashes. Live tests independently read the contracts with viem and reconcile totals with the vault's
807
+ own `getTotalAmounts()` after a sequential `poke()` simulation in `eth_call`. No transaction is sent.
808
+
809
+ ### Read a user's proportional vault position
334
810
 
335
- // Validate parameters
336
- client.validateDepositParams(params);
811
+ ```ts
812
+ const vault = await steer.getLiquidityVault(chainId, vaultAddress);
813
+ const position = await vault.getUserPosition(accountAddress);
814
+
815
+ position.shares; // Raw share balance (bigint)
816
+ position.totalSupply; // Raw share supply (bigint)
817
+ position.ownershipPercent; // Decimal string, e.g. "12.5"
818
+ position.amounts.token0; // { raw: bigint, formatted: string }
819
+ position.amounts.token1;
820
+ position.blockNumber;
337
821
  ```
338
822
 
339
- ### SingleAssetDepositClient Supported AMM Types
823
+ Pass `{ blockNumber }` to read a specific block; otherwise the operation pins one latest block.
824
+ The verified family snapshot, share balance, total supply and token decimals all use that block.
825
+ Each call performs native family verification before dispatching the resolved vault's snapshot override.
826
+ Overrides with or without `super` must retain the verified block, pool, token ordering, decimals and
827
+ position count; invalid results fail before share reads. `super` reuses the verified snapshot without
828
+ additional RPCs. The original binding and connected AMM adapter instances are retained.
829
+ The validation boundary copies and freezes snapshot values, including nested amounts and positions,
830
+ so later changes to an override's cached objects cannot change a pending user-position calculation.
831
+ Connected AMM adapters retain their original references and the override's objects remain untouched.
832
+ Amounts are each LP total multiplied by shares / total supply, rounded down in raw token units.
833
+ They include available idle, position principal and net uncollected fees, and exclude protocol and
834
+ strategist reserves. They are proportional accounting amounts, not executable withdrawal quotes.
835
+ Ownership percentages round down to 18 decimal places. A zero share balance returns zero amounts;
836
+ zero supply returns zero ownership and amounts. A share balance exceeding supply fails explicitly.
837
+
838
+ The normal address-reader path takes three pinned RPCs: proxy code verification, the existing family
839
+ snapshot reader, and one required Multicall for share balance and supply. Latest adds one head lookup;
840
+ compatibility snapshot paths retain their existing costs. Before Multicall deployment, or on an
841
+ unconfigured chain, the two share reads execute directly at the same block. Required failures propagate.
842
+
843
+ ### Read user positions for multiple vaults
340
844
 
341
- ```typescript
342
- enum AMMType {
343
- UniswapV3 = 0, // ✅ Fully implemented
344
- Algebra = 1, // 🚧 Planned
345
- AlgebraDirectional = 2, // 🚧 Planned
346
- AlgebraVE33 = 3, // 🚧 Planned
347
- AlgebraIntegral = 4, // 🚧 Planned
845
+ ```ts
846
+ const positions = await steer.getVaultUserPositions(
847
+ ChainId.Polygon,
848
+ vaultAddresses,
849
+ accountAddress,
850
+ { blockNumber }, // Optional; otherwise pin one latest block for the entire request.
851
+ );
852
+
853
+ for (const { vault, position } of positions) {
854
+ console.log(vault, position.shares, position.ownershipPercent, position.amounts);
348
855
  }
349
856
  ```
350
857
 
351
- Within `SingleAssetDepositClient`, only **UniswapV3** is fully implemented. This does not limit the separate pool-lifecycle APIs described above. Other single-asset-deposit AMM types will be added in future releases.
858
+ Results preserve input order and duplicates, including zero-balance positions. The bulk path uses
859
+ the same verified snapshots, registered vault/AMM overrides and proportional accounting as
860
+ `vault.getUserPosition()`. Unsupported implementations, identity mismatches and required RPC
861
+ failures reject the whole request. No partial portfolio is silently returned.
352
862
 
353
- ### Error Handling
863
+ On providers supporting vault-address overrides, ordinary small snapshots use two RPCs per
864
+ snapshot batch (up to 16 vaults and 128 positions), plus one required share/supply Multicall per
865
+ 64 requested vaults. Latest adds one head lookup for the whole request. Larger position sets and
866
+ compatibility paths retain their existing snapshot costs. Before Multicall deployment, or without
867
+ configured Multicall metadata, share reads use bounded direct calls. These limits bound requests;
868
+ they are not onchain vault limits. No cross-request identity or balance cache is introduced.
354
869
 
355
- All functions return a consistent `SteerResponse<T>` format:
870
+ ### Discover wallet-held vaults
356
871
 
357
- ```typescript
358
- interface SteerResponse<T> {
359
- data: T | null;
360
- status: number;
361
- success: boolean;
362
- error?: string;
363
- }
872
+ ```ts
873
+ const page = await steer.listUserVaults({
874
+ chainId: ChainId.Polygon,
875
+ account,
876
+ limit: 1000, // Maximum candidates scanned per page; default 1,000.
877
+ // Optional: amm, token, cursor
878
+ });
879
+ const positions = await steer.getVaultUserPositions(
880
+ ChainId.Polygon,
881
+ page.items,
882
+ account,
883
+ { blockNumber: page.indexedBlock },
884
+ );
885
+ ```
364
886
 
365
- // Example error handling
366
- const result = await vaultClient.previewSingleAssetDeposit(params, poolAddress);
367
- if (!result.success) {
368
- console.error('Preview failed:', result.error);
369
- return;
370
- }
887
+ Discovery scans real `balanceOf(account)` values for the indexed vault inventory, including shares
888
+ received by transfer. It does not trust `Depositor.shares`. All candidate pages and their balance
889
+ reads use one indexed block, returned as `indexedBlock`. Cursors bind the account, chain, endpoint,
890
+ registry, AMM/version and token filters to that snapshot. HTTP/indexer configuration and request
891
+ options work as for `listVaults()`. An RPC transport is required.
892
+
893
+ Each page scans at most `limit` candidates and returns only positive balances. **A page can be empty
894
+ and still have `nextCursor`; continue until the cursor is absent.** This bounds work even when most
895
+ vaults have zero shares. Smaller limits reduce provider payload/gas requirements. Recognized
896
+ `ScheduledJobs` and `DynamicJobs` records are excluded by exact name because they are not share
897
+ vaults. Unknown deployments stay candidates; failed/malformed balances reject the entire page,
898
+ without silently dropping addresses or inventing zeros.
899
+
900
+ A nonempty page normally costs one deployless Solidity `eth_call`; explicit unsupported execution
901
+ selects helper code overrides (two requests including the rejected probe), then Multicall (four
902
+ requests including both probes and chain validation). Without deployed Multicall metadata, fallback
903
+ uses bounded direct calls. Empty pages need no RPC. The first indexer page normally needs two HTTP
904
+ requests (metadata and candidates); continuations need one. No snapshots are loaded during discovery.
905
+
906
+ Results are ownership candidates, not verified implementation identities. Load details through
907
+ `getVault()` or `getVaultUserPositions()`, which retain identity checks and adapter overrides and
908
+ reject unsupported holdings explicitly. Inventory completeness depends on the indexer including all
909
+ relevant vaults. Holdings created or transferred after the indexed block are outside that snapshot;
910
+ staked and custodial shares are separate. No provider capability or balance cache is retained.
911
+
912
+ ### Preview and prepare a liquidity vault deposit
371
913
 
372
- // Use result.data safely
373
- console.log('LP tokens:', result.data.lpEstimation.lpTokens);
914
+ ```ts
915
+ const vault = await steer.getLiquidityVault(chainId, vaultAddress);
916
+ const request = {
917
+ account,
918
+ receiver: account, // Receives the shares; account supplies the tokens
919
+ assets: [
920
+ { asset: usdcAddress, amount: 100_000000n },
921
+ { asset: wethAddress, amount: 50_000000000000000n },
922
+ ],
923
+ };
924
+ const preview = await vault.previewDeposit(request, { blockNumber });
925
+ preview.blockNumber;
926
+ preview.shares; // Expected raw share units
927
+ preview.token0;
928
+ preview.token1;
929
+ preview.amounts.token0; // Accepted input: { raw: bigint, formatted: string }
930
+ preview.amounts.token1;
931
+ preview.unusedAmounts; // Requested inputs which remain in the wallet
932
+
933
+ const plan = await vault.prepareDeposit({ ...request, slippageBps: 50 });
934
+ plan.account;
935
+ plan.chainId;
936
+ plan.preview;
937
+ plan.minimumAmounts; // Minimum accepted amounts in token0/token1 order
938
+ plan.transactions; // Required capped approvals, then the unsigned deposit
374
939
  ```
375
940
 
376
- ### Common Error Cases
941
+ Both methods accept `{ blockNumber }` as the second argument. Omitting it pins one latest block.
942
+ Inputs are raw uint256 token units and represent maxima. Address order is arbitrary; the SDK maps
943
+ assets to the verified vault tokens. One omitted token has a zero maximum. Unknown/duplicate tokens,
944
+ negative amounts and all-zero inputs fail. These direct deposits do not swap tokens or wrap native
945
+ currency. A two-sided vault usually requires both inputs in its current holdings ratio.
377
946
 
378
- - **Invalid AMM Type**: `SingleAssetDepositClient` currently supports only UniswapV3
379
- - **Insufficient Liquidity**: Pool doesn't have enough liquidity for the swap
380
- - **Slippage Too High**: Price moved beyond acceptable limits
381
- - **Invalid Addresses**: Zero addresses or invalid contract addresses
382
- - **Amount Too Small**: Deposit amount too small to process
947
+ For a UI where the user enters one amount, ask the vault to infer the other token maximum:
383
948
 
384
- ### Testing
949
+ ```ts
950
+ const quote = await vault.quoteDeposit({
951
+ asset: usdcAddress,
952
+ amount: 100_000000n,
953
+ });
385
954
 
386
- Each function is designed to be independently testable:
955
+ quote.assets; // Maxima for both tokens, already in token0/token1 order
956
+ quote.shares; // Expected raw shares
957
+ quote.amounts; // Expected accepted amounts, raw and formatted
958
+ quote.unusedAmounts; // Any unused input caused by contract rounding
959
+ quote.blockNumber;
960
+
961
+ const plan = await vault.prepareDeposit({
962
+ account,
963
+ receiver: account,
964
+ assets: quote.assets,
965
+ slippageBps: 50,
966
+ });
967
+ ```
387
968
 
388
- ```typescript
389
- import { calculateSwapAmount, AMMType } from '@steerprotocol/sdk';
969
+ `quoteDeposit` needs no account or approvals. Pass `{ blockNumber }` as its optional second argument,
970
+ or omit it to pin one latest block. The selected input is a positive raw uint256 token maximum.
971
+ The other maximum is rounded up from the original vault's LP-owned holdings ratio, using bigint
972
+ arithmetic. This is not the pool's spot-price ratio and performs no swap. The quote uses the same
973
+ identity checks, registered snapshot/AMM overrides, original post-poke accounting and contract share
974
+ calculation as `previewDeposit`; it also takes three pinned/four latest RPCs. Unknown tokens, dust,
975
+ invalid backing and non-executable arithmetic reject explicitly.
976
+
977
+ An empty vault (zero share supply) has no established ratio, even if someone donated tokens. Its
978
+ one-token quote rejects with guidance to choose explicit maxima through `previewDeposit`, preserving
979
+ the implementation's first-mint rules. A funded one-sided vault quotes zero for the absent token
980
+ when the selected token is the accepted side; selecting the absent side rejects. The contract can
981
+ leave a small unused amount after rounding a one-sided deposit's share mint. Quote assets and all
982
+ returned amounts are immutable. Preparation re-reads state, so refresh quotes when conditions change
983
+ or explicitly pin both operations to the same block when comparing them.
984
+
985
+ Previews work without balances or approvals. Native family identity checks and validated snapshot/AMM
986
+ overrides run first. The SDK then calls the original vault's `poke()` and `getTotalAmounts()` sequentially
987
+ in one Multicall `eth_call`, with supply, pause and registry-state reads pinned to that block. The fee
988
+ checkpoint is discarded after the call. Share pricing uses these original contract totals, never
989
+ custom snapshot totals. Token decimals come from the detached, identity-validated snapshot.
990
+
991
+ The shared bigint formula follows the exact deployed `getShares`/`_calcSharesAndAmounts` code,
992
+ including token rounding up, share rounding down, checked cross-product/FullMath bounds and first
993
+ share minting. Qualified older Polygon Uniswap/Sushi/Retro V3 implementations permit any positive
994
+ first share mint; Base V3, Integral and Legacy require 1,000,000 raw initial shares. The exact
995
+ implementation addresses and runtime hashes remain in the family identity files. Sources were
996
+ verified using Sourcify for all eight mapped implementations, including the
997
+ [Base V3 implementation](https://sourcify.dev/server/v2/contract/8453/0xBDDe75BBf4a580c5E564B5f9aC7B12007dD307C6?fields=all),
998
+ [older Polygon V3 implementation](https://sourcify.dev/server/v2/contract/137/0x434bf4902C0dcd515BC76A302f20C9c45b844b1b?fields=all),
999
+ and [historical QuickSwap implementation](https://sourcify.dev/server/v2/contract/137/0x4603E812Ef2454E72E57661f0aA39D7d20e9A968?fields=all).
1000
+
1001
+ Paused vaults and registry states other than PendingThreshold/Active reject deposits. Bootstrap
1002
+ minimums, zero-share quotes, invalid backing and arithmetic overflow fail explicitly. A preview is
1003
+ a contract-compatible quote, not confirmation of wallet funding or token transfer readiness.
1004
+
1005
+ Preparation obtains its own quote and reads both allowances in the same aggregate. It reuses
1006
+ sufficient approvals; otherwise it encodes approvals capped at the requested maxima, resetting a
1007
+ nonzero insufficient allowance first. Submit transactions in order from `plan.account` on
1008
+ `plan.chainId`, confirming approvals before submitting the deposit. The SDK signs and submits nothing.
1009
+ The application supplies gas, fee and nonce settings through its wallet.
1010
+
1011
+ `slippageBps` is required, from 0 through 10,000. Each minimum is the accepted raw amount discounted
1012
+ by that tolerance and rounded down. The ABI protects minimum accepted token amounts, not minimum
1013
+ shares; these are not equivalent protections. At 10,000 both minima are zero. Values describe the
1014
+ quoted block; execution can fail after state or wallet balances change.
1015
+
1016
+ Both methods use **3 pinned RPCs**, or **4** for latest, on the normal reader path. Allowances are
1017
+ included only for preparation, without adding a round trip. Snapshot compatibility paths keep their
1018
+ existing costs plus one original-accounting Multicall. That aggregate must be deployed at the selected
1019
+ block; required failures propagate without retries or invented accounting. Results, captured inputs
1020
+ and unsigned requests are immutable. Live tests verify unfunded quotes and execute original approvals
1021
+ and deposits with real funded accounts through rolled-back `eth_call` simulations only.
1022
+
1023
+ ### Preview and prepare a liquidity vault withdrawal
390
1024
 
391
- describe('Single-Asset Deposit', () => {
392
- it('should calculate correct swap amount', async () => {
393
- const result = await calculateSwapAmount(publicClient, {
394
- depositAmount: parseEther('100'),
395
- isToken0: true,
396
- vault: vaultAddress,
397
- pool: poolAddress,
398
- ammType: AMMType.UniswapV3,
399
- singleAssetDepositContract: contractAddress,
400
- });
1025
+ ```ts
1026
+ const vault = await steer.getLiquidityVault(chainId, vaultAddress);
1027
+ const request = { account, shares: 1000n, receiver: account };
1028
+ const preview = await vault.previewWithdraw(request, { blockNumber });
1029
+
1030
+ preview.blockNumber;
1031
+ preview.shares;
1032
+ preview.token0;
1033
+ preview.token1;
1034
+ preview.amounts.token0; // { raw: bigint, formatted: string }
1035
+ preview.amounts.token1;
1036
+
1037
+ const plan = await vault.prepareWithdraw({ ...request, slippageBps: 50 });
1038
+ plan.account; // Must submit the transaction: this account owns the shares
1039
+ plan.chainId;
1040
+ plan.preview;
1041
+ plan.minimumAmounts; // Raw and formatted minimum outputs for both tokens
1042
+ plan.transactions; // Unsigned { to, data, value } requests
1043
+ ```
401
1044
 
402
- expect(result.success).toBe(true);
403
- expect(result.data.swapAmount).toBeGreaterThan(0n);
404
- });
405
- });
1045
+ Both methods accept `{ blockNumber }` as their optional second argument; omitting it pins one
1046
+ latest block. Shares are positive raw uint256 units. `account` and `receiver` must be nonzero
1047
+ addresses. The vault burns the sender's shares, so a different account cannot submit this payload
1048
+ on the holder's behalf. `slippageBps` is required for preparation and must be an integer from 0
1049
+ through 10,000; minimum token amounts round down in raw units. At 10,000 both minima are zero.
1050
+ Small amounts can also round to zero. The application chooses its tolerance and submission time;
1051
+ a preview describes its block, and later execution can revert if conditions change.
1052
+
1053
+ V3, Legacy Algebra (including the qualified historical QuickSwap beacon) and Integral vaults use
1054
+ their original deployed `withdraw()` through `eth_call`, with the real account and receiver. This
1055
+ simulates burns, collection, fees and token transfers, rather than estimating a proportion of the
1056
+ accounting snapshot. Insufficient shares, transfer failures and contract reverts propagate. No wallet,
1057
+ approval, balance override, signature or transaction submission is needed for simulation.
1058
+
1059
+ Each method independently verifies native vault identity and dispatches registered snapshot/AMM
1060
+ overrides at the same block. Overridden snapshots retain the existing identity validation and detached
1061
+ metadata capture. Preparation obtains its own native simulation and does not reuse an earlier preview
1062
+ or call a public `previewWithdraw` override. Results and unsigned requests are immutable.
1063
+
1064
+ The normal path uses **3 pinned RPC requests**, or **4** for latest: proxy code verification, the
1065
+ existing verified snapshot reader and one original withdrawal simulation. Resolution is separate.
1066
+ Snapshot compatibility fallbacks remain available with one additional withdrawal call; simulation
1067
+ errors never trigger fallback or an alternative accounting estimate. Live tests simulate both the
1068
+ preview and prepared calldata without broadcasting, and verify the holder's shares stay unchanged.
1069
+
1070
+ ### QuickSwap Integral liquidity vaults on Base
1071
+
1072
+ The same consumer getters automatically resolve the mapped Integral vault and connect its AMM:
1073
+
1074
+ ```ts
1075
+ const vault = await steer.getLiquidityVault(ChainId.Base, integralVaultAddress);
1076
+ const snapshot = await vault.getSnapshot();
1077
+
1078
+ vault.amm.protocol; // AmmProtocol.QuickSwap
1079
+ vault.amm.version; // QuickSwapVersion.V4
1080
+ vault.amm.subgraphUrl;
1081
+ snapshot.positions;
1082
+ snapshot.idle;
1083
+ snapshot.principal;
1084
+ snapshot.fees; // gross, net, recorded, each with token0/token1 raw and formatted amounts.
1085
+ snapshot.feeRateBps;
1086
+ snapshot.total;
406
1087
  ```
407
1088
 
408
- ### Configuration Parameters
1089
+ This qualifies Base beacon `0x532654AbC884b4FA00Ffd5c29bbd40eC4669473B` and implementation
1090
+ `0x391cf3047db983caB8dD95B16c4e6C3918d72d23`. Registry membership, actual proxy beacon,
1091
+ proxy/implementation runtime hashes, stored registry, factory/pool-deployer registration and
1092
+ vault/pool token ordering are checked at the selected block. Snapshots independently verify
1093
+ pool identity even when an AMM adapter overrides `getPool()`. Each invocation reads fresh state.
1094
+
1095
+ Integral uses packed owner/lower/upper position keys, rather than V3 hashes. Historical ranges do
1096
+ not need to align to the pool's current mint spacing. Position liquidity is checked against the
1097
+ uint128 accounting bound, despite the Integral position ABI exposing uint256. Global fee growth
1098
+ is read once and shared tick boundaries are deduplicated. Required failures propagate.
1099
+
1100
+ The verified implementation stores its registry at slot 251 and fee manager at slot 257; it does
1101
+ not use V3's accounting helper. Fee cuts round down per position, and recorded fees are already
1102
+ included in gross. The shared snapshot accounting returns idle + principal + net fees as `total`.
1103
+ Live tests reconcile these totals against the funded three-position vault
1104
+ `0x2cf9fb4eeb424e4a0160fc44857cfcf5cd09a93b` using `poke()` followed by `getTotalAmounts()`
1105
+ in a single ephemeral `eth_call`. No transaction is submitted.
1106
+
1107
+ Vault snapshots prefer `IntegralVaultAddressReader`: original proxy code verification followed by
1108
+ one code-only override at the vault address. Chain/registry/beacon/implementation, exact layout,
1109
+ factory/pool-deployer registration, tokens, accounting and lock checks happen inside the reader.
1110
+ Original getters delegate to the verified implementation; storage and implementation code stay
1111
+ unchanged. The default AMM adapter consumes invocation-bound verified pool data; registered
1112
+ getPool overrides always run and their returned identity is checked independently.
1113
+
1114
+ A pinned snapshot uses **2 RPC requests**, or **3** for latest. Separate pinned resolution uses
1115
+ **2**, making a cold pinned resolve plus snapshot **4**. If address overrides are explicitly
1116
+ unsupported, bounded Multicall and the existing AMM reader remain available. Including the proxy
1117
+ preflight and rejected override probe, fallback snapshots use **20** requests with a deployless pool
1118
+ reader, **21** with a helper override, or **25** through full compatibility. Large fallback
1119
+ configurations add bounded position/tick batches. Empty ranges skip position/tick/growth reads.
1120
+ Deposit and withdrawal previews and unsigned preparation use the same API as V3 and Legacy above.
1121
+ No Graph credentials are required for vault reads.
1122
+
1123
+ ## Extension points
1124
+
1125
+ | Base | Responsibility |
1126
+ | --- | --- |
1127
+ | `UniswapV3Adapter` | Address-based pool reads and V3 quote interface |
1128
+ | `UniswapV4Adapter` | Reusable V4 pool-key/ID reads and quote implementation |
1129
+ | `AaveV3Adapter` | Reserve reads and supply transaction preparation interface |
1130
+ | `PoolDataAdapter`, `LendingDataAdapter` | Indexed protocol discovery |
1131
+ | `SteerClient` | SDK entry point, network routing, and typed integration lookup |
1132
+ | `SteerNetwork`, `SteerDataAdapter` | Verified vault loading and indexed vault discovery |
1133
+ | `VaultResolver`, `VaultIdentityReader` | Exact implementation resolution and pinned identity reads |
1134
+ | `SteerVault` | Identity, shares, and strategy positions |
1135
+
1136
+ Vault categories describe strategy exposure, not an ERC-4626 or multi-asset ABI. Every vault has
1137
+ consistent direct methods: `previewDeposit`, `prepareDeposit`, and `prepareWithdraw`. Deposits use
1138
+ `{ account, assets: [{ asset, amount }], receiver }`; liquidity previews also return block, token and amount details. Withdrawals use shares,
1139
+ account, and receiver. Concrete implementations enforce accepted assets and map these requests to
1140
+ actual contracts. For example, an ERC-4626 implementation maps a share-denominated exit to redeem,
1141
+ not to the ABI's asset-denominated withdraw amount. Supported liquidity vaults implement withdrawal
1142
+ encoding; lending and composite transaction operations remain scaffolds.
1143
+
1144
+ A vault's `underlying` field contains the configured protocol references from loading time, while
1145
+ `getPositions()` queries actual positions. Empty vaults can still have configured underlyings. Mixed
1146
+ positions use `PositionKind` to distinguish liquidity, lending, and idle assets. A composite vault
1147
+ has no single `amm` property. There are no capability objects or capability checks.
1148
+
1149
+ ## Validation
1150
+
1151
+ Prerelease branch, version calculation and npm authorization setup are documented in
1152
+ [RELEASING.md](RELEASING.md). Local version previews do not publish or verify publishing credentials.
1153
+
1154
+ Tests follow the SDK areas, with separate directories for offline and live suites:
1155
+
1156
+ ```text
1157
+ tests/
1158
+ unit/ # amm, catalog, core, data, http, observability, vault, and runner config
1159
+ integration/
1160
+ amm/uniswap-v3.base.test.ts
1161
+ fixtures/ # shared offline deployment/adapter/vault fixtures
1162
+ support/ # runner configuration helpers
1163
+ consumer/ # isolated packed-package Node, TypeScript, and Next.js fixture
1164
+ ```
409
1165
 
410
- | Parameter | Type | Description |
411
- | ---------------------------- | --------- | ----------------------------------------- |
412
- | `assets` | `bigint` | Amount of input token to deposit |
413
- | `receiver` | `Address` | Address to receive LP tokens |
414
- | `vault` | `Address` | Vault contract address |
415
- | `isToken0` | `boolean` | `true` for token0, `false` for token1 |
416
- | `depositSlippagePercent` | `bigint` | Max slippage for deposit (in %) |
417
- | `swapSlippageBP` | `number` | Slippage for internal swap (basis points) |
418
- | `ammType` | `AMMType` | Type of AMM (currently UniswapV3) |
419
- | `singleAssetDepositContract` | `Address` | Single-asset deposit contract address |
1166
+ Both suites use `.test.ts`. `vitest.config.ts` selects only `tests/unit/`, while
1167
+ `vitest.integration.config.ts` selects only `tests/integration/`. New tests go beside tests for the
1168
+ same SDK area; shared fixtures and runner helpers stay outside the discovery paths.
1169
+
1170
+ Run `npm run check` and `npm run format:check` from this directory. The default tests run offline
1171
+ with fake RPC/HTTP responses; live tests don't run during `test`, `test:watch`, `test:coverage`,
1172
+ `check`, or packaging.
1173
+
1174
+ After `npm run check` builds the package, run `npm run check:consumer` to verify the npm tarball
1175
+ from a temporary consumer project. It checks ESM imports, CJS requires, strict TypeScript NodeNext
1176
+ resolution for both declaration formats, and a Next.js production build with server and client
1177
+ components. It also checks the rendered SDK chain metadata. The fixture has its own pinned
1178
+ dependencies and lockfile; it does not use the legacy SDK or repository module resolution.
1179
+ This command requires npm registry access to install consumer dependencies, but no RPC or indexer
1180
+ credentials. The dedicated Next SDK CI workflow runs both commands on Node 22.23.4, 24.22.1 and 26.12.1.
1181
+
1182
+ Use `nvm install` and `nvm use` from the repository root to select the default Node 24.22.1 runtime.
1183
+ Both consumer and tooling requirements accept Node ^22.23.4, ^24.22.1 or ^26.12.1; older patches
1184
+ and unsupported major lines fall outside the declared runtime support. These minimums follow the
1185
+ [October 21, 2026 Node security release](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases),
1186
+ which includes CA-certificate and TLS fixes. Keep the actual deployed application runtime patched;
1187
+ package metadata does not upgrade Node for consumers.
1188
+
1189
+ Run the live suite separately from `next/`. Put local credentials in `.env.integration`, using
1190
+ `.env.integration.example` as the template, then run:
1191
+
1192
+ ```sh
1193
+ npm run test:integration
1194
+ ```
420
1195
 
421
- ### Architecture
1196
+ The command uses Node's built-in `--env-file-if-exists` support; it doesn't need dotenv. Git ignores
1197
+ `.env.integration`. The example contains empty values for `BASE_RPC_URL`, `THE_GRAPH_API_KEY`, and
1198
+ `THE_GRAPH_PROXY_URL`; fill in the RPC URL and exactly one Graph setting. Don't commit credentials.
1199
+
1200
+ Exported shell variables and CI-injected variables take precedence over file values. CI can run
1201
+ the same command without a local file. If neither source provides valid configuration, the command
1202
+ still fails before making requests.
1203
+
1204
+ For an application-owned proxy, unset `THE_GRAPH_API_KEY` and set `THE_GRAPH_PROXY_URL` to an
1205
+ absolute HTTP(S) URL. The route must implement the existing `subgraphId` query-parameter contract
1206
+ and provide its own Graph authentication. Relative browser paths such as `/api/the-graph` won't
1207
+ work in the Node test runner. When switching authentication modes, clear the other setting in both
1208
+ the file and environment so exactly one remains nonempty. The command loads only `.env.integration`;
1209
+ it doesn't automatically load other `.env` files.
1210
+
1211
+ The command requires `BASE_RPC_URL` and exactly one Graph setting. Missing, conflicting, or invalid
1212
+ configuration fails before tests start; there are no automatic skips or public endpoint fallbacks.
1213
+ Use a Base mainnet RPC that supports `eth_call` at a recent explicit block. Tests pin RPC reads and
1214
+ quotes to the block selected at suite startup. The subgraph retains its own indexed snapshot;
1215
+ it need not have caught up to the RPC head.
1216
+
1217
+ Live tests verify Multicall3 routing and RPC counts through the configured transport, WETH/USDC
1218
+ pool discovery, direct reads, decoded prices and fee percentage against
1219
+ independent same-block viem reads, both pool token balances with exact decimal formatting, and a
1220
+ small QuoterV2 quote. They list pools
1221
+ by token address, symbol, and name, check token matches onchain, verify duplicate-free pagination
1222
+ at one indexed block, and inspect structured events from real indexer/HTTP calls. They perform
1223
+ reads and `eth_call` simulations only. Tests don't sign or submit transactions, and they assert
1224
+ the SDK quote against an independent viem QuoterV2 simulation at the same block, without
1225
+ hard-coding a price. Independent viem token reads check that both pagination pages still contain
1226
+ USDC when the second request supplies only a cursor. Test providers must serve the configured
1227
+ Uniswap V3 Base subgraph and have indexed enough USDC pools for two pages.
1228
+
1229
+ The live harness omits provider error details that might echo credentials. HTTP failures retain
1230
+ their status; inspect the provider privately when a live call fails. Credential redaction assertions
1231
+ compare booleans so test failures don't print secret values. Live runs can fail because of provider
1232
+ outages, rate limits, indexer changes, or deployment changes, independently of offline tests.
1233
+
1234
+ Default tests also cover typed lookup, family reference types, registration errors, endpoint URLs,
1235
+ vault composition, logging, and parity with the prior SDK's chain inventory. See `AGENTS.md` for
1236
+ portability rules and the separate consumer and tooling Node requirements.
1237
+
1238
+ ## Uniswap V4 on Base
1239
+
1240
+ The V4 AMM adapter supports pool reads, indexed pool search, pool-key utilities and single-pool
1241
+ exact-input quotes. Use the same client transport, `indexers`, HTTP defaults and optional logger
1242
+ as V3. Standalone V4 vault operations are qualified separately on BNB Chain below.
422
1243
 
423
- The module is built with a modular architecture where each step is a separate, testable function:
1244
+ ```ts
1245
+ const amm = steer.getAmm({
1246
+ chainId: ChainId.Base,
1247
+ protocol: AmmProtocol.Uniswap, version: UniswapVersion.V4,
1248
+ });
424
1249
 
1250
+ const page = await amm.listPools({ token: { symbol: "USDC" }, limit: 20 });
1251
+ // Address and name searches work too, including the zero address for native ETH.
1252
+ await amm.listPools({ token: { address: usdcAddress } });
1253
+ await amm.listPools({ token: { name: "USD Coin" } });
1254
+
1255
+ for (const key of page.items) {
1256
+ const id = amm.getPoolId(key); // Pure ABI encoding and hashing; no network request.
1257
+ const recovered = await amm.getPoolKey(id); // Verified indexed metadata lookup.
1258
+ const pool = await amm.getPool(key); // Complete keys require RPC only.
1259
+ await amm.getPool({ poolId: id }); // Resolves metadata, then verifies onchain state.
1260
+
1261
+ const quote = await amm.quote({
1262
+ pool: recovered,
1263
+ inputCurrency: key.currency0,
1264
+ outputCurrency: key.currency1,
1265
+ amountIn, // Raw units of inputCurrency.
1266
+ hookData: "0x", // Supply the hook's required bytes when applicable.
1267
+ });
1268
+ }
425
1269
  ```
426
- src/base/vault/single-asset/
427
- ├── types.ts # TypeScript interfaces and enums
428
- ├── calculateSwapAmount.ts # Step 1: Calculate swap amount
429
- ├── calculateLimitPrice.ts # Step 2: Calculate slippage protection
430
- ├── simulateSwap.ts # Step 3: Simulate swap execution
431
- ├── estimateLpTokens.ts # Step 4: Estimate LP token output
432
- ├── singleAssetDeposit.ts # Main orchestrator client
433
- └── index.ts # Exports everything
1270
+
1271
+ A pool key contains `currency0`, `currency1`, `fee`, `tickSpacing`, and `hooks`. Currencies must be
1272
+ sorted numerically. Key validation includes V4 fee flags and hook permission relationships.
1273
+ Pool IDs are `keccak256(abi.encode(key))`; the hash cannot be reversed. ID-only reads require the
1274
+ configured indexer to recover metadata and verify the complete key against its ID. Missing keys
1275
+ fail clearly; supplying a complete key avoids that dependency. Per-request HTTP overrides are
1276
+ accepted by `getPoolKey`, ID-based `getPool`, `quote`, and `listPools`, including
1277
+ ID-based family discovery through `steer.getPool`.
1278
+
1279
+ Listings return complete immutable keys, with snapshot-bound, strictly ordered, duplicate-free
1280
+ pagination. Token address matches are exact; symbols and names use case-insensitive substring
1281
+ matching. Combined filters match the same token. The two token sides are queried independently
1282
+ in one GraphQL request, then merged in ID order. The indexer's `feeTier` may contain the latest
1283
+ swap fee rather than the key's original fee. Dynamic flags are verified by hash; other changed
1284
+ fees are recovered from immutable PoolManager Initialize logs at the indexed creation block.
1285
+ Recovery requires the configured RPC transport, verifies its chain, and groups up to 50 pool IDs
1286
+ per log query at the same block. It verifies every event address, block and key hash; missing or
1287
+ conflicting events fail clearly. Ordinary indexed listings still require no RPC calls.
1288
+
1289
+ Snapshots expose `key`, `poolId`, `poolManager`, raw price/tick/liquidity, currency
1290
+ decimals, native-currency flags, decoded decimal-string prices, fees and `blockNumber`. Price
1291
+ field `currency0InCurrency1` corresponds to currency0 in currency1, including native ETH. Native ETH
1292
+ has 18 decimals on Base; ERC-20 decimals are read at the snapshot block. Fees retain the configured
1293
+ key fee, dynamic flag, current stored LP fee and packed protocol fees with each direction decoded.
1294
+ `lpFeePercent` describes the stored LP fee, not a guaranteed total swap charge. Hooks may override
1295
+ fees or alter accounting during execution, so use the actual Quoter simulation for swap amounts.
1296
+ Hook failures propagate; a quote is not a guarantee that every caller can execute a swap.
1297
+
1298
+ V4 pools do not have individual contract addresses. PoolManager token balances cover many pools,
1299
+ so V4 snapshots intentionally do not expose V3-style pool balances or invent per-pool reserves.
1300
+ The adapter verifies chain identity, StateView/Quoter manager bindings, pool initialization and
1301
+ returned adapter identities at the selected block. The internal Solidity reader at
1302
+ `src/amm/adapters/uniswap-v4/reader/V4PoolReader.sol` combines StateView state, manager validation and
1303
+ ERC-20 decimals in one ephemeral execution. Native ETH uses 18 decimals without a token call.
1304
+ It tries deployless execution, then a helper-code override only after explicit unsupported-feature
1305
+ rejection, and uses Multicall3 only when both modes explicitly reject support. Identity failures,
1306
+ reverts, invalid responses and provider errors propagate without fallback. No transaction is sent
1307
+ and no code/storage is persisted or replaced on target contracts.
1308
+ Pinned complete-key reads and family discovery use 2 RPC requests, including chain validation.
1309
+ An override-only provider uses 3; full Multicall compatibility uses 5, including both rejected
1310
+ reader attempts. Pinned quotes remain at 4, without reader or token-metadata calls. Latest reads add one block-number request. Indexer lookups
1311
+ are HTTP requests and do not change these RPC counts unless static fee recovery needs initialization
1312
+ logs. Recovery adds one chain check and one log request per group of at most 50 IDs in a creation block.
1313
+
1314
+ Family discovery remains available when choosing between configured V4 deployments:
1315
+
1316
+ ```ts
1317
+ const pool = await steer.getPool({
1318
+ chainId: ChainId.Base,
1319
+ family: AmmFamily.UniswapV4,
1320
+ reference: { poolId }, // A complete key is also accepted.
1321
+ // poolManager: managerAddress, // Required if multiple configured managers are ambiguous.
1322
+ });
434
1323
  ```
435
1324
 
436
- This design allows for:
1325
+ `UniswapV4Adapter` is the reusable family base: extend it for compatible protocols or override
1326
+ methods for behavioral differences. `UniswapV4` supplies the protocol identity. Chain support comes
1327
+ from deployment configuration; neither class hard-codes a chain allowlist. The AMM and data-adapter
1328
+ classes and V4 models live together in `src/amm/adapters/uniswap-v4/`.
1329
+
1330
+ `fees` uses explicit units: `configuredFee` retains the PoolKey fee/flag, `lpFeePips` and
1331
+ `lpFeePercent` describe the stored LP fee, and `protocolFeePacked` preserves the raw packed value.
1332
+ Each direction exposes `protocolFeePips` and `protocolFeePercent`. One pip is one millionth.
1333
+ `PoolPrice`, `V4PoolFees`, and `V4DirectionalFees` are named public models.
437
1334
 
438
- - **Individual Testing**: Each function can be unit tested separately
439
- - **Reusability**: Functions can be used in different contexts
440
- - **Maintainability**: Easy to update or extend individual components
441
- - **Debugging**: Easy to isolate issues to specific steps
1335
+ V4 contracts are registered on Base only. Deployment sources:
1336
+ [Uniswap deployments](https://developers.uniswap.org/docs/protocols/v4/deployments),
1337
+ [PoolIdLibrary](https://github.com/Uniswap/v4-core/blob/main/src/types/PoolId.sol),
1338
+ [StateView](https://github.com/Uniswap/v4-periphery/blob/main/src/lens/StateView.sol), and
1339
+ [V4 subgraph mappings](https://github.com/Uniswap/v4-subgraph/tree/main/src/mappings).
442
1340
 
443
- ## Requirements
1341
+ V3 and V4 prices share `PoolPrice` with `currency0InCurrency1` and `currency1InCurrency0`.
1342
+ V3 may return `price: undefined` for an uninitialized pool; successful V4 reads always include a price.
444
1343
 
445
- - Node.js >= 18.0.0
446
- - viem >= 2.22.0
1344
+ All pool snapshots share `PoolState.dynamicFee`. Canonical Uniswap V3 reports `false`.
1345
+ Uniswap V4 derives it from the PoolKey's `0x800000` flag, including when the current LP fee is zero.
1346
+ A hook alone does not imply dynamic fees. The hook address remains available as `pool.key.hooks`.
1347
+ Other protocol adapters must derive the flag from their own fee mechanism; protocol-fee changes
1348
+ alone do not make a pool's LP fee dynamic. V4's previous `fees.dynamic` field is now `pool.dynamicFee`.
447
1349
 
448
- ## Development
1350
+ ## Pool ticks and liquidity distribution
449
1351
 
450
- 1. Install dependencies:
1352
+ Both AMM adapters expose the same tick API. V3 uses its pool address; V4 accepts a PoolKey or
1353
+ `{ poolId }`, with the existing indexer configuration and per-request HTTP overrides for ID lookup.
451
1354
 
452
- ```bash
453
- npm install
1355
+ ```ts
1356
+ const options = { blockNumber }; // Omit to pin the latest block once for the operation.
1357
+ const specific = await amm.getTicks(reference, [-120, 0, 120], options);
1358
+ const range = { lowerTick: lowerVisibleTick, upperTick: upperVisibleTick };
1359
+ const window = await amm.getSurroundingTicks(reference, range, options);
1360
+ const distribution = await amm.getLiquidityDistribution(reference, range, options);
1361
+
1362
+ // Reuse an already fetched window without another RPC call.
1363
+ const sameDistribution = buildLiquidityDistribution(window);
1364
+ const depth = calculateLiquidityDepth(distribution, {
1365
+ fromPrice: "1.25",
1366
+ toPrice: "1.50",
1367
+ });
1368
+ depth.amount0; // { raw: bigint, formatted: string }
1369
+ depth.amount1;
454
1370
  ```
455
1371
 
456
- 2. Build the package:
1372
+ `getTicks` accepts 1 to 1024 tick indices, deduplicates and sorts them, and returns an entry for
1373
+ each requested tick. Uninitialized ticks have `initialized: false` and zero gross/net liquidity.
1374
+ `getSurroundingTicks` returns all initialized ticks within the inclusive bounds, including
1375
+ initialized ticks with zero net liquidity. Ranges must cover at most 64 bitmap words. Ticks are
1376
+ integers from -887272 through 887272. Neither method fabricates empty words after failed reads.
1377
+
1378
+ Both results contain `pool` with the operation's tick, Q96 square-root price, active liquidity,
1379
+ shared `dynamicFee` flag,
1380
+ tick spacing, currency decimals and block number. This is a minimal mathematical snapshot; it
1381
+ does not fetch balances. Tick and pool state reads share one block. Family readers preserve V3
1382
+ factory allowlists/registration and V4 StateView manager/key validation before reading tick data.
1383
+
1384
+ Small pinned reads use 2 RPC requests: chain validation and one ephemeral Solidity execution.
1385
+ Each execution scans at most four bitmap words and returns at most 128 tick records, keeping
1386
+ return data below the deployless EVM code-size limit. Larger requests continue automatically at
1387
+ the same block without repeating chain validation. Latest reads add one block-number request.
1388
+ An override-only provider adds one rejected deployless attempt. Providers rejecting both reader
1389
+ modes use batched bitmap and tick calls through the existing compatibility path; errors and
1390
+ reverts do not trigger fallback. V4 ID resolution can additionally need indexer requests and
1391
+ initialization logs, as documented above. There is no cross-operation transport capability cache.
1392
+
1393
+ Distribution ranges must include the stored current tick. Intervals retain exact Q96 bounds,
1394
+ bigint active liquidity and reciprocal decimal-string prices. Liquidity is added when crossing
1395
+ initialized ticks to the right and removed when crossing to the left; inconsistent negative
1396
+ liquidity fails instead of being clamped. Empty initialized-tick windows can still have constant
1397
+ active liquidity. Rendering, numeric chart conversion, inversion, labels and overlays stay in the UI.
1398
+
1399
+ Depth prices are positive decimal strings in currency1 per whole currency0, without exponent
1400
+ notation. Calculations intersect the entire selected range with the distribution and return both
1401
+ asset amounts, rounded down per interval. The SDK's published endpoint prices restore their exact
1402
+ Q96 boundaries. Requests outside fetched coverage fail instead of returning a partial amount.
1403
+ This is fee-free geometric liquidity depth, not an executable swap quote or hook simulation.
1404
+
1405
+ ### QuickSwap Integral on Base
1406
+
1407
+ ```ts
1408
+ const amm = steer.getAmm({
1409
+ chainId: ChainId.Base,
1410
+ protocol: AmmProtocol.QuickSwap, version: QuickSwapVersion.V4,
1411
+ });
1412
+
1413
+ const page = await amm.listPools({ token: { symbol: "USDC" } });
1414
+ const reference = page.items[0];
1415
+ if (!reference) throw new Error("No matching pools");
457
1416
 
458
- ```bash
459
- npm run build
1417
+ const pool = await amm.getPool(reference);
1418
+ const quote = await amm.quote({
1419
+ pool: reference,
1420
+ inputCurrency: pool.token0,
1421
+ outputCurrency: pool.token1,
1422
+ amountIn: 10n ** BigInt(pool.token0Decimals), // One whole token0.
1423
+ });
1424
+ const ticks = await amm.getSurroundingTicks(reference, {
1425
+ lowerTick: pool.tick - 100,
1426
+ upperTick: pool.tick + 100,
1427
+ }, { blockNumber: pool.blockNumber });
460
1428
  ```
461
1429
 
462
- 3. Run tests:
1430
+ Ordinary pools accept an address. Custom pools use `{ address, deployer }`; listings return the
1431
+ appropriate reference so it can be passed directly to reads, quotes and tick methods. The adapter
1432
+ verifies the factory, its pool deployer and the matching base/custom registration at the selected
1433
+ block. Quotes also verify the quoter's factory and pool-deployer bindings.
1434
+
1435
+ Pool snapshots retain raw state and add decoded prices, token balances and explicit fee units.
1436
+ `fees.lastFeePips` is the stored fee and can be stale when dynamic fees are enabled; it is not a
1437
+ promise of the fee used by a future swap. `dynamicFee` comes from Integral's configuration bit,
1438
+ not plugin presence. Quotes use the deployed QuoterV2 and reject partial input consumption.
1439
+
1440
+ Integral tick tables use raw tick indexes. Existing ticks remain valid when mint spacing changes;
1441
+ Integral tick totals use uint256. The normalized chart types and distribution/depth utilities are
1442
+ shared with V3/V4, whose stricter tick checks remain in their family readers. Scans retain the
1443
+ 64-word range limit and 4-word/128-record pages. A pinned snapshot or one-page tick read uses
1444
+ 2 RPC requests with deployless execution, 3 with override-only support. Full compatibility uses
1445
+ 7 for snapshots, 8 for specific ticks and 9 for a nonempty one-page range. Quotes use 5.
1446
+ Latest-block selection adds one request; larger scans add bounded pages.
1447
+
1448
+ Deployment qualification used the [published QuickSwap Base addresses](https://docs.quickswap.exchange/overview/contracts-and-addresses)
1449
+ and live checks at Base block 52733010: factory `0xC5396866754799B9720125B104AE01d935Ab9C7b`,
1450
+ pool deployer `0xE08026Fd8537d67C501199610c42D08bB34eAa75`, and QuoterV2
1451
+ `0x23E0583a3a000d567bB3848115065c1890D87fb5`. The WETH/USDC pool is
1452
+ `0x5a9Ad2BB92B0B3E5C571FDD5125114E04E02be1a`. Its six-field `globalState`, six-field tick tuple,
1453
+ quoter return layout, factory registrations and bytecode hashes are covered by the separate live
1454
+ suite. This qualifies the deployed Integral interface, without inferring a version from QuickSwap's
1455
+ "V4" branding. Additional Algebra deployments and vault implementations require separate qualification. QuickSwap Integral Base vault reads are qualified below.
1456
+
1457
+ ### Legacy Algebra: QuickSwap on Polygon and Camelot on Arbitrum
463
1458
 
464
- ```bash
465
- npm test
1459
+ ```ts
1460
+ const quickswap = steer.getAmm({
1461
+ chainId: ChainId.Polygon,
1462
+ protocol: AmmProtocol.QuickSwap, version: QuickSwapVersion.V3,
1463
+ });
1464
+ const camelot = steer.getAmm({
1465
+ chainId: ChainId.Arbitrum,
1466
+ protocol: AmmProtocol.Camelot, version: CamelotVersion.V3,
1467
+ });
1468
+ const page = await camelot.listPools({ token: { symbol: "USDC" } });
1469
+ const address = page.items[0];
1470
+ if (!address) throw new Error("No matching pools");
1471
+ const pool = await camelot.getPool(address);
1472
+ const fee0To1 = pool.fees.currency0ToCurrency1.lastFeePercent;
1473
+ const fee1To0 = pool.fees.currency1ToCurrency0.lastFeePercent;
1474
+
1475
+ // Resolve the protocol from the pool's registered factory within the known family.
1476
+ const discovered = await steer.getPool({
1477
+ chainId: ChainId.Arbitrum,
1478
+ family: AmmFamily.AlgebraLegacy,
1479
+ address,
1480
+ });
466
1481
  ```
467
1482
 
468
- 4. Format code:
1483
+ Both protocols use address references and expose `getPool`, `listPools`, `quote`, `getTicks`,
1484
+ `getSurroundingTicks` and `getLiquidityDistribution`. Listings support token address, symbol and
1485
+ name, with snapshot pagination and the same client `indexers` and per-request HTTP overrides.
1486
+ Snapshots include raw state, decimal-string prices, and raw/formatted balances at the same block.
1487
+
1488
+ `AlgebraLegacyAdapter` implements the qualified QuickSwap V1 interface. `AlgebraDirectionalAdapter`
1489
+ extends it for Camelot's directional V1.9 interface, and `Camelot` selects that implementation.
1490
+ `QuickSwap` selects symmetric V1. Deployment bindings select the implementation explicitly;
1491
+ no runtime version detection or protocol-name matching is performed. Integral retains its existing
1492
+ classes and public API. Shared Algebra contracts, pool fields, RPC batching and invocation-local
1493
+ discovery live in `amm/adapters/algebra/`; ABIs, fee decoding and readers stay in the family folders.
1494
+
1495
+ Legacy snapshots expose stored fees for **both swap directions**. They are equal for symmetric
1496
+ QuickSwap pools and can differ for Camelot. `dynamicFee: true` identifies these deployments'
1497
+ adaptive fee mechanism. Stored fees can differ from the next swap's fee. Community fee shares
1498
+ are reported separately for token0 and token1 in permille and percentage units. QuoterV2 is checked
1499
+ against the configured factory and pool deployer. Its four-value ABI does not return consumed
1500
+ input; quotes reject results at the default price boundaries where full consumption is uncertain.
1501
+
1502
+ QuickSwap V1 bitmaps compress ticks by the fixed spacing of 60. Camelot uses raw tick indexes,
1503
+ so historical ticks need not align with its current mint spacing. Both readers preserve uint128
1504
+ legacy tick liquidity, bounded bitmap pages and the shared chart/depth API. This distinction is
1505
+ implemented by subclass overrides, without adding consumer configuration.
1506
+
1507
+ Pinned snapshots use 2 RPC requests with deployless execution, 3 with overrides and 8 with full
1508
+ compatibility fallback. One-page specific tick reads use 2/3/9; nonempty range scans use 2/3/10.
1509
+ Quotes use 5 requests. Latest-block selection adds one request, discovery adds one factory read,
1510
+ and larger tick reads add bounded pages. Identity checks remain active on every invocation.
1511
+
1512
+ Qualification uses [QuickSwap's published contracts](https://docs.quickswap.exchange/overview/contracts-and-addresses)
1513
+ and [Camelot's legacy AMMv3 contracts](https://docs.camelot.exchange/contracts/arbitrum/one-mainnet/).
1514
+ Independent live tests verify factory/deployer/quoter bindings, code hashes, registration, both
1515
+ fee layouts, token accounting, quotes in both directions, bitmap parity, chart depth and listing.
1516
+ Camelot's newer Integral AMMv4 factory is a separate deployment and is not registered here.
1517
+ Set `POLYGON_RPC_URL` and `ARBITRUM_RPC_URL` in `.env.integration` or CI; they share the existing
1518
+ Graph API-key or proxy configuration. SDK runtime configuration never reads this file.
1519
+
1520
+ ### Common quoting and fee terminology
1521
+
1522
+ Every AMM exposes `quote(request, options)` through `AmmAdapter<TPoolRef>`. The request carries
1523
+ `pool`, `inputCurrency`, `outputCurrency` and `amountIn`; the result contains raw bigint
1524
+ `amountIn` and `amountOut`. Concrete V4 adapters additionally accept optional `hookData`.
1525
+ Quotes keep their family-specific routing, identity checks, read options and RPC budgets.
1526
+
1527
+ Pool fee data lives under `pool.fees`. Directional fields consistently use
1528
+ `currency0ToCurrency1` and `currency1ToCurrency0`, including pools with native currencies.
1529
+ Rates retain explicit units and meanings:
1530
+
1531
+ | Family | Swap/LP fee fields | Additional fields |
1532
+ | --- | --- | --- |
1533
+ | Uniswap V3 | `fees.swapFeePips`, `fees.swapFeePercent` | Fixed swap fee, including any protocol share deducted from it |
1534
+ | Uniswap V4 | `fees.lpFeePips`, `fees.lpFeePercent` | Configured key fee, packed protocol fee and per-direction protocol fee rates |
1535
+ | Algebra Legacy | `fees.currency0ToCurrency1.lastFeePips/lastFeePercent` and the reverse direction | Token-specific community shares in permille and percentage |
1536
+ | Algebra Integral | `fees.lastFeePips`, `fees.lastFeePercent` | Community share in permille and percentage |
1537
+
1538
+ Stored dynamic fees are not promises of the next swap's execution fee. V4's LP fee and protocol
1539
+ fees remain separate; Algebra community shares are cuts of collected swap fees. The new SDK uses
1540
+ these names directly: V3's former top-level `fee`/`feePercent` and the former directional names
1541
+ are replaced without compatibility aliases. Raw onchain values remain available in the fee fields.
1542
+
1543
+ ## PancakeSwap on BNB Chain
1544
+
1545
+ The built-in BNB Chain catalog registers `AmmProtocol.PancakeSwap` with
1546
+ `PancakeSwapVersion.V3` and `PancakeSwapVersion.InfinityCL`. V3 reuses the V3 family, including the wider Pancake
1547
+ `slot0` protocol-fee field. Infinity CL has a separate family and implementation under
1548
+ `src/amm/adapters/pancakeswap-infinity-cl/`. Infinity Bin and Pancake vault support are outside
1549
+ this implementation.
469
1550
 
470
- ```bash
471
- npm run format
1551
+ ```ts
1552
+ const steer = new SteerClient({
1553
+ transports: { [ChainId.BSC]: rpcTransport },
1554
+ indexers: { theGraph: { apiKey: graphApiKey } },
1555
+ });
1556
+ const pancake = steer.getAmm({
1557
+ chainId: ChainId.BSC,
1558
+ protocol: AmmProtocol.PancakeSwap, version: PancakeSwapVersion.V3,
1559
+ });
1560
+ const infinity = steer.getAmm({
1561
+ chainId: ChainId.BSC,
1562
+ protocol: AmmProtocol.PancakeSwap, version: PancakeSwapVersion.InfinityCL,
1563
+ });
1564
+
1565
+ const pool = await pancake.getPool(poolAddress);
1566
+ const indexed = await infinity.listPools({ token: { symbol: "USDT" }, limit: 10 });
1567
+ // Infinity listings return { poolId }; canonical keys resolve directly from the manager.
1568
+ const reference = indexed.items[0];
1569
+ if (!reference) throw new Error("No matching Infinity CL pools");
1570
+ const infinityPool = await infinity.getPool(reference);
1571
+ const key = await infinity.getPoolKey(infinityPool.poolId);
1572
+ const id = infinity.getPoolId(key);
1573
+ const ticks = await infinity.getSurroundingTicks(key, { lowerTick: -100, upperTick: 100 });
472
1574
  ```
473
1575
 
474
- 5. Lint code:
1576
+ `getPoolKey` needs RPC, while
1577
+ `getPoolId` and `createPoolKey` are local utilities. `createPoolKey` accepts `currency0`,
1578
+ `currency1`, `hooks`, `poolManager`, `fee`, `tickSpacing`, and optional `hookPermissions`,
1579
+ and packs Infinity's `parameters` field. Currency ordering is validated, never silently changed.
1580
+ Infinity keys hash all six protocol fields, including manager and parameters. Native BNB uses
1581
+ the zero address and stays distinct from WBNB.
1582
+
1583
+ Both adapters support snapshots, exact-input quotes, specific ticks, initialized tick ranges,
1584
+ liquidity distributions, and indexed pool listing by token address, symbol, or name. Infinity
1585
+ quotes accept optional `hookData` and reject input amounts outside positive `int128` range.
1586
+ Snapshots expose decoded prices, raw fees and percentages, and the shared `dynamicFee` flag.
1587
+ Infinity's Vault holds shared reserves; its snapshot does not expose those as pool balances.
1588
+
1589
+ | Pinned read | Pancake V3 | Infinity CL |
1590
+ | --- | --- | --- |
1591
+ | Snapshot | 2 / 3 / 7 | 2 / 3 / 5 |
1592
+ | Specific ticks, one page | 2 / 3 / 8 | 2 / 3 / 6 |
1593
+ | Nonempty initialized range, one page | 2 / 3 / 9 | 2 / 3 / 7 |
1594
+ | Exact-input quote | 5 | 4 |
1595
+
1596
+ Counts show deployless / override / Multicall compatibility, including rejected feature probes.
1597
+ Latest-block reads add one RPC. Infinity ID and full-key references use the same budgets;
1598
+ the reader resolves and verifies the canonical key within its existing call. V3 discovery
1599
+ adds one factory read; Infinity discovery shares its context with the selected adapter. Larger
1600
+ scans add bounded pages. No reader reverts or required-member failures trigger a compatibility retry.
1601
+
1602
+ Deployments come from [Pancake V3 addresses](https://developer.pancakeswap.finance/contracts/v3/addresses)
1603
+ and [Infinity addresses](https://developer.pancakeswap.finance/contracts/infinity/resources/addresses).
1604
+ V3 uses the public [grafted V3 repair deployment](https://thegraph.com/explorer/subgraphs/G4vqYZxFaL7driMegHynL9RywXgbdWEVwR1Y7gb9YSLD).
1605
+ Its Messari schema exposes `liquidityPools` and `inputTokens`. The family-local
1606
+ `PancakeSwapV3DataAdapter` maps that schema to the existing address listing API, validates protocol
1607
+ identity against the configured factory, and keeps token address/symbol/name filters, pinned
1608
+ pagination, client indexer configuration, and request overrides. Listings require no RPC reads;
1609
+ onchain pool reads still independently verify factory registration.
1610
+
1611
+ The configured [Infinity CL subgraph](https://thegraph.com/explorer/subgraphs/8jFYxwKP8tNGSDisucpHRK1ojUchZd7ELd8zh2ugHGDN)
1612
+ is used only for listings and currently reports indexing errors. Listing rejects these failures
1613
+ explicitly; contract operations do not depend on indexer availability. A configured data-adapter
1614
+ override can supply another compatible indexer.
1615
+
1616
+ Set `BSC_RPC_URL` in `.env.integration` or CI to run the BNB live tests. The existing Graph API key
1617
+ or proxy configuration is reused. Live listing tests remain enabled and fail when their indexers
1618
+ are unhealthy.
1619
+
1620
+ ## Standalone Uniswap V4 vaults on BNB Chain
1621
+
1622
+ The qualified `MultiPositionReserveV4` deployment supports the existing liquidity-vault
1623
+ methods, including same-block positions, idle balances, fees, wallet shares, bulk reads,
1624
+ deposit quotes/previews, and unsigned deposit/withdrawal preparation. Hook and parking
1625
+ vaults and other standalone implementations remain unsupported.
475
1626
 
476
- ```bash
477
- npm run lint
1627
+ ```ts
1628
+ const vault = await steer.getLiquidityVault(
1629
+ ChainId.BSC,
1630
+ "0x0700dc95d5f17438b2e3e1a5fd2b085a648d91ba",
1631
+ );
1632
+ const snapshot = await vault.getSnapshot();
1633
+ if (vault instanceof UniswapV4Vault) {
1634
+ const { key, poolId, poolManager } = vault.pool;
1635
+ const pool = await vault.amm.getPool(key);
1636
+ }
1637
+ const holdings = await vault.getUserPosition(account);
1638
+ const quote = await vault.quoteDeposit({ asset: snapshot.token0, amount });
1639
+ const plan = await vault.prepareDeposit({
1640
+ account, receiver: account, assets: quote.assets, slippageBps: 50,
1641
+ });
478
1642
  ```
479
1643
 
480
- ## Troubleshooting
1644
+ V4 snapshot `pool` is `{ key, poolId, poolManager }`; existing V3/Algebra snapshot pools
1645
+ remain addresses. Concrete family classes preserve those types in snapshots, positions
1646
+ and user holdings; heterogeneous client/bulk results keep the union. Native currency uses address zero, 18 decimals and real native balances.
1647
+ Native deposits carry the requested maximum as transaction value and skip native approvals.
1648
+ Unused value is refunded by the original vault. All returned transactions are unsigned.
481
1649
 
482
- ### CommonJS/ESM Import Issues
1650
+ This initial family implementation uses original contract reads and the existing V4 AMM
1651
+ reader with its deployless/override/Multicall fallback. A pinned resolved snapshot takes
1652
+ 16 RPCs normally or 19 through compatibility; latest adds one. Bulk reads use bounded
1653
+ compatibility orchestration, not the optimized two-RPC V3/Algebra vault readers. BNB AMM
1654
+ listing/ID lookup is explicitly unavailable without a qualified indexer; full-key reads work.
1655
+ Steer indexed V4 beacon candidates are available, but token metadata is incomplete and
1656
+ V4 token search is not qualified. Direct address-based reads are independently verified.
1657
+ See [deployment qualification and budgets](src/vault/adapters/uniswap-v4/qualification.md).
483
1658
 
484
- If you encounter the following error:
1659
+ ### Aerodrome Slipstream on Base
485
1660
 
486
- ```typescript
487
- import { SteerClient } from "@steerprotocol/sdk";
488
- ^^^^^^^^^^^
489
- SyntaxError: Named export 'SteerClient' not found. The requested module '@steerprotocol/sdk' is a CommonJS module...
1661
+ ```ts
1662
+ const amm = steer.getAmm({
1663
+ chainId: ChainId.Base,
1664
+ protocol: AmmProtocol.Aerodrome,
1665
+ version: AerodromeVersion.SlipstreamV3,
1666
+ });
1667
+ const pool = await amm.getPool(poolAddress);
1668
+ const ticks = await amm.getSurroundingTicks(poolAddress, { lowerTick, upperTick });
1669
+ const quote = await amm.quote({
1670
+ pool: poolAddress, inputCurrency: pool.token0, outputCurrency: pool.token1, amountIn,
1671
+ }, { account }); // Optional caller for fee-module discounts.
490
1672
  ```
491
1673
 
492
- Add the following to your `tsconfig.json`:
1674
+ Use Slipstream, SlipstreamV2 or SlipstreamV3 explicitly; omitting version is ambiguous. All three
1675
+ support pool snapshots/batches, direct quotes, specific/surrounding ticks and liquidity charts.
1676
+ Pool references are addresses. Identity is verified using token pair plus tick spacing, not swap fee.
1677
+ Snapshots include raw/formatted pool balances, prices, observed swap fees, unstaked-fee allocation,
1678
+ gauge and NFT-manager addresses, and stakedLiquidity as a subset of total liquidity. dynamicFee is
1679
+ true because Slipstream uses mutable fee mechanisms; an observed fee is not a later execution guarantee.
493
1680
 
494
- ```json
495
- {
496
- "compilerOptions": {
497
- "esModuleInterop": true,
498
- "allowSyntheticDefaultImports": true
499
- }
500
- }
1681
+ All three generations use the public Steer catalog for discovery. No Graph API key is required.
1682
+ The call stays the same:
1683
+
1684
+ ```ts
1685
+ const page = await amm.listPools({ token: { symbol: "USDC" }, limit: 100 });
1686
+ page.items; // Pool addresses, still candidates until verified by getPool().
1687
+ page.freshness; // { revision, generatedAt, blockNumber, blockHash?, source }
1688
+ const next = page.nextCursor
1689
+ ? await amm.listPools({ cursor: page.nextCursor, limit: 100 })
1690
+ : undefined;
501
1691
  ```
502
1692
 
503
- ## License
1693
+ Token address, symbol and name filters match either side; combined fields match the same token.
1694
+ Pages are address-ordered, with limits from 1 to 1000. Cursors bind endpoint, chain, factory,
1695
+ filters and catalog revision. A publication between pages causes an explicit restart error,
1696
+ not mixed revisions. Each page costs one HTTP request and zero RPCs. getPool independently
1697
+ verifies factory registration using the existing reader and its unchanged RPC budget.
504
1698
 
505
- MIT
1699
+ Freshness reports the collection time/block and whether data came from KV (`stored`), a retained
1700
+ KV result (`retained`), or the bundled snapshot (`bundled`). Block/hash refer to catalog collection,
1701
+ not a promise that every cached fee or token field was refreshed then. Sugar can omit pools with
1702
+ no token0 liquidity; this is discovery coverage, not every factory-created address. No live fee,
1703
+ volume ranking or complete inventory is implied. subgraphUrl is undefined for these API adapters.
1704
+ Client HTTP defaults and per-request fetch/header/cancellation overrides remain supported.
1705
+
1706
+ The production app API supports operation `listPools` at `/api/aerodrome/pools`. All seven
1707
+ Aerodrome live tests passed against production and Base RPC on 2026-10-20. Older API responses
1708
+ fail explicitly. For local or preview live tests, set `STEER_CATALOG_URL` to the updated app
1709
+ endpoint; it affects tests only.
1710
+ AMM deployment details are in [qualification](src/amm/adapters/aerodrome/qualification.md).
1711
+
1712
+ ### Aerodrome liquidity vaults on Base
1713
+
1714
+ ```ts
1715
+ const vault = await steer.getLiquidityVault(ChainId.Base, vaultAddress);
1716
+ const snapshot = await vault.getSnapshot();
1717
+
1718
+ snapshot.positions; // Actual staked NFT IDs, ranges, weights, principal and recorded pair fees.
1719
+ snapshot.configuredPositions; // Strategy ranges, including currently empty ranges.
1720
+ snapshot.reservedRewards; // Collected rewards excluded from LP idle when AERO overlaps the pair.
1721
+ snapshot.rewards; // Separate reward asset, unclaimed rewards and unpaid Steer/strategist fees.
1722
+
1723
+ const page = await steer.listVaults({
1724
+ chainId: ChainId.Base,
1725
+ amm: { protocol: AmmProtocol.Aerodrome, version: AerodromeVersion.SlipstreamV2 },
1726
+ token: { symbol: "USDC" },
1727
+ });
1728
+ ```
1729
+
1730
+ The original, V2 and V3 AMMs resolve automatically from exact beacon/implementation mappings and
1731
+ actual pool factories. One beacon supports both original and V2 factories. Version-filtered listing
1732
+ therefore checks vault pool factories at the indexed block, adding one RPC per candidate page on
1733
+ the normal path. Unfiltered listing remains HTTP-only. Limits apply to scanned candidates, so an
1734
+ empty filtered page can still have nextCursor. Listing candidates still require getVault identity checks.
1735
+
1736
+ LP totals exclude collected reward reserves and keep gauge rewards separate from the token pair.
1737
+ Snapshots preserve actual NFT tokensOwed without inventing V3 swap-fee accrual for staked NFTs.
1738
+ Small pinned resolutions, resolved snapshots and bulk snapshots each take two RPCs; latest adds one.
1739
+ Strict original-storage/Multicall compatibility remains available. Standalone claims and reward-debt
1740
+ user-read APIs remain unqualified.
1741
+
1742
+ ```ts
1743
+ const withdrawal = await vault.previewWithdraw({ account, shares, receiver });
1744
+ withdrawal.amounts; // Raw/formatted token0 and token1 proceeds paid to receiver.
1745
+ withdrawal.rewards; // Aerodrome: { token, recipient: account, amount: { raw, formatted } }.
1746
+
1747
+ const plan = await vault.prepareWithdraw({ account, shares, receiver, slippageBps: 50 });
1748
+ // Submit plan.transactions from plan.account on plan.chainId using your wallet integration.
1749
+ ```
1750
+
1751
+ Withdrawals pay all accrued shareholder rewards, including when only some shares are burned.
1752
+ Rewards remain separate from LP proceeds even when AERO is a pool token. Preparation simulates
1753
+ original execution again and returns one unsigned withdrawal. Slippage protects the LP amounts;
1754
+ the contract has no reward-minimum parameter. Previews/preparation each cost three pinned RPCs,
1755
+ or four with latest block selection; original-contract failures remain explicit.
1756
+ Deposits use the original pre-settlement totals and the exact verified helper. The same one-token
1757
+ quote, preview and unsigned preparation API is supported:
1758
+
1759
+ ```ts
1760
+ const quote = await vault.quoteDeposit({ asset: token0, amount });
1761
+ const deposit = await vault.previewDeposit({ account, receiver, assets: quote.assets });
1762
+ deposit.rewards; // Aerodrome: existing receiver rewards, settled before new shares are minted.
1763
+ const depositPlan = await vault.prepareDeposit({
1764
+ account, receiver, assets: quote.assets, slippageBps: 50,
1765
+ });
1766
+ ```
506
1767
 
507
- ## Pancake v4 / Sushi Infinity on Robinhood
1768
+ The token owner supplies the assets; pre-existing rewards go to the share receiver. New shares do
1769
+ not earn past rewards. Quotes have no receiver and omit rewards. Preview/preparation return a
1770
+ separate raw/formatted reward payout, even when AERO is a deposited asset. They use three pinned
1771
+ RPCs or four for latest, without requiring wallet funding for the quote. Preparation checks
1772
+ allowances, adds capped approvals/reset approvals when needed, and appends one unsigned deposit.
1773
+ Slippage protects accepted token amounts, not minimum minted shares or reward amounts. No swap,
1774
+ standalone claim, signing or broadcasting occurs. Empty vaults require explicit asset maxima;
1775
+ Aerodrome's qualified helper accepts one raw initial share. One-sided vaults quote only the held asset.
508
1776
 
509
- Use `Protocol.SushiV4` with protocol family `pancake-v4` for the Infinity CL deployment and `SushiV4DynamicFeeALM` beacon. The app supplies a hardcoded Infinity pool list; pool state is read onchain through the CL manager. The configured Goldsky endpoint currently indexes Sushi v3 data.
1777
+ See [vault qualification, accounting and budgets](src/vault/adapters/aerodrome/qualification.md).