@steerprotocol/sdk 3.8.1 → 4.0.0-next.11

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