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