@steerprotocol/sdk 3.8.0 → 4.0.0-next.10

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