@steerprotocol/liquidity-meter 1.0.0 → 2.0.4
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 +27 -56
- package/bin/liquidity-depth.js +795 -0
- package/package.json +42 -32
- package/src/api.js +568 -0
- package/src/depth.js +492 -0
- package/src/index.js +6 -0
- package/src/prices.js +188 -0
- package/src/uniswapv3-subgraph.js +141 -0
- package/src/viem-onchain.js +506 -0
- package/src/withdraw.js +333 -0
- package/templates/README.md +37 -0
- package/templates/pools_minimal.csv +3 -0
- package/templates/pools_with_vault.csv +3 -0
- package/dist/adapters/onchain.d.ts +0 -51
- package/dist/adapters/onchain.js +0 -158
- package/dist/adapters/uniswapv3-subgraph.d.ts +0 -44
- package/dist/adapters/uniswapv3-subgraph.js +0 -105
- package/dist/adapters/withdraw.d.ts +0 -14
- package/dist/adapters/withdraw.js +0 -150
- package/dist/api.d.ts +0 -72
- package/dist/api.js +0 -180
- package/dist/cli/liquidity-depth.d.ts +0 -2
- package/dist/cli/liquidity-depth.js +0 -1160
- package/dist/core/depth.d.ts +0 -48
- package/dist/core/depth.js +0 -314
- package/dist/handlers/cron.d.ts +0 -27
- package/dist/handlers/cron.js +0 -68
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -7
- package/dist/prices.d.ts +0 -15
- package/dist/prices.js +0 -205
- package/dist/wagmi/config.d.ts +0 -2106
- package/dist/wagmi/config.js +0 -24
- package/dist/wagmi/generated.d.ts +0 -2019
- package/dist/wagmi/generated.js +0 -346
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Uniswap v3 subgraph client (axios-based)
|
|
2
|
+
// Pulls pool snapshot and ticks in a target range.
|
|
3
|
+
|
|
4
|
+
import { DepthInternals } from './depth.js';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SUBGRAPH = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3';
|
|
8
|
+
|
|
9
|
+
function percentToTickDelta(pct) {
|
|
10
|
+
const r = 1 + pct / 100;
|
|
11
|
+
const delta = Math.log(r) / Math.log(1.0001);
|
|
12
|
+
return Math.max(1, Math.round(delta));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function graphqlFetch(url, query, variables) {
|
|
16
|
+
const res = await axios.post(url, { query, variables }, {
|
|
17
|
+
headers: { 'content-type': 'application/json' },
|
|
18
|
+
maxRedirects: 5,
|
|
19
|
+
timeout: 15000,
|
|
20
|
+
validateStatus: (s) => s >= 200 && s < 300,
|
|
21
|
+
});
|
|
22
|
+
const body = res.data;
|
|
23
|
+
if (body.errors) {
|
|
24
|
+
const msg = body.errors.map((e) => e.message).join('; ');
|
|
25
|
+
throw new Error(`Subgraph error: ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
return body.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchPoolSnapshot({
|
|
31
|
+
poolAddress,
|
|
32
|
+
percentBuckets = [1, 2, 5, 10],
|
|
33
|
+
subgraphUrl = DEFAULT_SUBGRAPH,
|
|
34
|
+
blockNumber,
|
|
35
|
+
}) {
|
|
36
|
+
const id = poolAddress.toLowerCase();
|
|
37
|
+
let blockVar = null;
|
|
38
|
+
if (blockNumber !== undefined && blockNumber !== null) {
|
|
39
|
+
const bn = typeof blockNumber === 'bigint' ? blockNumber : BigInt(blockNumber);
|
|
40
|
+
if (bn < 0n) throw new Error('Block number must be non-negative');
|
|
41
|
+
const asNumber = Number(bn);
|
|
42
|
+
if (!Number.isFinite(asNumber)) throw new Error('Block number is too large for subgraph query');
|
|
43
|
+
blockVar = { number: asNumber };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// First fetch pool core fields + ETH price
|
|
47
|
+
const coreQuery = `
|
|
48
|
+
query PoolCore($id: ID!, $block: Block_height) {
|
|
49
|
+
bundle(id: "1", block: $block) { ethPriceUSD }
|
|
50
|
+
pool(id: $id, block: $block) {
|
|
51
|
+
id
|
|
52
|
+
sqrtPrice
|
|
53
|
+
tick
|
|
54
|
+
liquidity
|
|
55
|
+
feeTier
|
|
56
|
+
token0 { id symbol decimals derivedETH }
|
|
57
|
+
token1 { id symbol decimals derivedETH }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const core = await graphqlFetch(subgraphUrl, coreQuery, { id, block: blockVar });
|
|
63
|
+
if (!core.pool) throw new Error(`Pool not found in subgraph: ${id}`);
|
|
64
|
+
|
|
65
|
+
const ethPriceUSD = Number(core.bundle?.ethPriceUSD ?? 0);
|
|
66
|
+
const pool = core.pool;
|
|
67
|
+
|
|
68
|
+
const sqrtPriceX96 = BigInt(pool.sqrtPrice);
|
|
69
|
+
const tick = Number(pool.tick);
|
|
70
|
+
const liquidity = BigInt(pool.liquidity);
|
|
71
|
+
const feePips = Number(pool.feeTier);
|
|
72
|
+
|
|
73
|
+
const token0 = {
|
|
74
|
+
decimals: Number(pool.token0.decimals),
|
|
75
|
+
usdPrice: Number(pool.token0.derivedETH) * ethPriceUSD,
|
|
76
|
+
symbol: pool.token0.symbol,
|
|
77
|
+
id: pool.token0.id,
|
|
78
|
+
};
|
|
79
|
+
const token1 = {
|
|
80
|
+
decimals: Number(pool.token1.decimals),
|
|
81
|
+
usdPrice: Number(pool.token1.derivedETH) * ethPriceUSD,
|
|
82
|
+
symbol: pool.token1.symbol,
|
|
83
|
+
id: pool.token1.id,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Determine tick bounds for the farthest bucket
|
|
87
|
+
const far = Math.max(...percentBuckets);
|
|
88
|
+
const delta = percentToTickDelta(far);
|
|
89
|
+
const start = Math.max(DepthInternals.MIN_TICK, tick - delta);
|
|
90
|
+
const end = Math.min(DepthInternals.MAX_TICK, tick + delta);
|
|
91
|
+
|
|
92
|
+
// Paginate ticks in [start, end]
|
|
93
|
+
const ticks = [];
|
|
94
|
+
const pageSize = 1000;
|
|
95
|
+
let skip = 0;
|
|
96
|
+
while (true) {
|
|
97
|
+
const ticksQuery = `
|
|
98
|
+
query PoolTicks($id: ID!, $start: Int!, $end: Int!, $first: Int!, $skip: Int!, $block: Block_height) {
|
|
99
|
+
pool(id: $id, block: $block) {
|
|
100
|
+
ticks(where: { tickIdx_gte: $start, tickIdx_lte: $end }, orderBy: tickIdx, orderDirection: asc, first: $first, skip: $skip) {
|
|
101
|
+
tickIdx
|
|
102
|
+
liquidityNet
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
const data = await graphqlFetch(subgraphUrl, ticksQuery, {
|
|
108
|
+
id,
|
|
109
|
+
start,
|
|
110
|
+
end,
|
|
111
|
+
first: pageSize,
|
|
112
|
+
skip,
|
|
113
|
+
block: blockVar,
|
|
114
|
+
});
|
|
115
|
+
const page = data.pool?.ticks ?? [];
|
|
116
|
+
for (const t of page) {
|
|
117
|
+
ticks.push({ index: Number(t.tickIdx), liquidityNet: BigInt(t.liquidityNet) });
|
|
118
|
+
}
|
|
119
|
+
if (page.length < pageSize) break;
|
|
120
|
+
skip += pageSize;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
sqrtPriceX96,
|
|
125
|
+
tick,
|
|
126
|
+
liquidity,
|
|
127
|
+
feePips,
|
|
128
|
+
ticks,
|
|
129
|
+
token0: { decimals: token0.decimals, usdPrice: token0.usdPrice },
|
|
130
|
+
token1: { decimals: token1.decimals, usdPrice: token1.usdPrice },
|
|
131
|
+
meta: {
|
|
132
|
+
token0,
|
|
133
|
+
token1,
|
|
134
|
+
ethPriceUSD,
|
|
135
|
+
poolId: id,
|
|
136
|
+
range: { start, end },
|
|
137
|
+
farPercent: far,
|
|
138
|
+
blockNumber: blockVar?.number ?? undefined,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
// On-chain fetcher for Uniswap v3 pools using a viem public client.
|
|
2
|
+
// It prefers JSON-RPC batching when an RPC URL is available to reduce HTTP round trips.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPublicClient,
|
|
6
|
+
decodeFunctionResult,
|
|
7
|
+
encodeFunctionData,
|
|
8
|
+
getAddress,
|
|
9
|
+
http,
|
|
10
|
+
parseAbi,
|
|
11
|
+
} from 'viem';
|
|
12
|
+
|
|
13
|
+
const UNISWAP_POOL_ABI = parseAbi([
|
|
14
|
+
'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
|
|
15
|
+
'function liquidity() view returns (uint128)',
|
|
16
|
+
'function fee() view returns (uint24)',
|
|
17
|
+
'function tickSpacing() view returns (int24)',
|
|
18
|
+
'function token0() view returns (address)',
|
|
19
|
+
'function token1() view returns (address)',
|
|
20
|
+
'function tickBitmap(int16) view returns (uint256)',
|
|
21
|
+
'function ticks(int24 tick) view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized)'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const ALGEBRA_POOL_ABI = parseAbi([
|
|
25
|
+
'function liquidity() view returns (uint128)',
|
|
26
|
+
'function tickSpacing() view returns (int24)',
|
|
27
|
+
'function token0() view returns (address)',
|
|
28
|
+
'function token1() view returns (address)',
|
|
29
|
+
'function tickTable(int16) view returns (uint256)',
|
|
30
|
+
'function ticks(int24 tick) view returns (uint256 liquidityTotal, int128 liquidityDelta, int24 prevTick, int24 nextTick, uint256 outerFeeGrowth0Token, uint256 outerFeeGrowth1Token)'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const ERC20_ABI = parseAbi([
|
|
34
|
+
'function decimals() view returns (uint8)',
|
|
35
|
+
'function symbol() view returns (string)'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const GLOBAL_STATE_SELECTOR = '0xe76c01e4';
|
|
39
|
+
const TWO_256 = 1n << 256n;
|
|
40
|
+
const TWO_255 = 1n << 255n;
|
|
41
|
+
const poolStaticCache = new Map();
|
|
42
|
+
const tokenMetaCache = new Map();
|
|
43
|
+
|
|
44
|
+
function toRpcBlockTag(blockTag) {
|
|
45
|
+
if (typeof blockTag === 'bigint') return `0x${blockTag.toString(16)}`;
|
|
46
|
+
if (typeof blockTag === 'number') return `0x${blockTag.toString(16)}`;
|
|
47
|
+
return blockTag ?? 'latest';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function batchRpcRequest({ rpcUrl, requests }) {
|
|
51
|
+
if (!rpcUrl) throw new Error('rpcUrl is required for batched RPC requests');
|
|
52
|
+
const res = await fetch(rpcUrl, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'content-type': 'application/json' },
|
|
55
|
+
body: JSON.stringify(requests),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`Batched RPC request failed with HTTP ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
const body = await res.json();
|
|
61
|
+
const responses = Array.isArray(body) ? body : [body];
|
|
62
|
+
const byId = new Map(responses.map((entry) => [entry.id, entry]));
|
|
63
|
+
return requests.map((request) => {
|
|
64
|
+
const response = byId.get(request.id);
|
|
65
|
+
if (!response) {
|
|
66
|
+
throw new Error(`Missing batched RPC response for id ${request.id}`);
|
|
67
|
+
}
|
|
68
|
+
if (response.error) {
|
|
69
|
+
throw new Error(response.error.message || JSON.stringify(response.error));
|
|
70
|
+
}
|
|
71
|
+
return response.result;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readContractsBatch({ rpcUrl, blockTag, address, abi, calls }) {
|
|
76
|
+
const normalizedAddress = getAddress(address);
|
|
77
|
+
return readContractCallsBatch({
|
|
78
|
+
rpcUrl,
|
|
79
|
+
blockTag,
|
|
80
|
+
calls: calls.map((call) => ({ ...call, address: normalizedAddress, abi })),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readContractCallsBatch({ rpcUrl, blockTag, calls }) {
|
|
85
|
+
const rpcBlockTag = toRpcBlockTag(blockTag);
|
|
86
|
+
const requests = calls.map((call, index) => ({
|
|
87
|
+
jsonrpc: '2.0',
|
|
88
|
+
id: index + 1,
|
|
89
|
+
method: 'eth_call',
|
|
90
|
+
params: [
|
|
91
|
+
{
|
|
92
|
+
to: getAddress(call.address),
|
|
93
|
+
data: encodeFunctionData({
|
|
94
|
+
abi: call.abi,
|
|
95
|
+
functionName: call.functionName,
|
|
96
|
+
args: call.args,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
rpcBlockTag,
|
|
100
|
+
],
|
|
101
|
+
}));
|
|
102
|
+
const results = await batchRpcRequest({ rpcUrl, requests });
|
|
103
|
+
return results.map((result, index) =>
|
|
104
|
+
decodeFunctionResult({
|
|
105
|
+
abi: calls[index].abi,
|
|
106
|
+
functionName: calls[index].functionName,
|
|
107
|
+
data: result,
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readContracts({ client, rpcUrl, blockTag, address, abi, calls, readBlockParams }) {
|
|
113
|
+
if (rpcUrl) {
|
|
114
|
+
try {
|
|
115
|
+
return await readContractsBatch({ rpcUrl, blockTag, address, abi, calls });
|
|
116
|
+
} catch (_) {}
|
|
117
|
+
}
|
|
118
|
+
return Promise.all(
|
|
119
|
+
calls.map((call) =>
|
|
120
|
+
client.readContract({
|
|
121
|
+
address,
|
|
122
|
+
abi,
|
|
123
|
+
functionName: call.functionName,
|
|
124
|
+
args: call.args,
|
|
125
|
+
...readBlockParams,
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readRawCall({ client, rpcUrl, to, data, blockTag }) {
|
|
132
|
+
if (rpcUrl) {
|
|
133
|
+
try {
|
|
134
|
+
const [result] = await batchRpcRequest({
|
|
135
|
+
rpcUrl,
|
|
136
|
+
requests: [
|
|
137
|
+
{
|
|
138
|
+
jsonrpc: '2.0',
|
|
139
|
+
id: 1,
|
|
140
|
+
method: 'eth_call',
|
|
141
|
+
params: [{ to, data }, toRpcBlockTag(blockTag)],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
return result;
|
|
146
|
+
} catch (_) {}
|
|
147
|
+
}
|
|
148
|
+
return client.transport.request({
|
|
149
|
+
method: 'eth_call',
|
|
150
|
+
params: [{ to, data }, toRpcBlockTag(blockTag)],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function chunk(array, size) {
|
|
155
|
+
const out = [];
|
|
156
|
+
for (let i = 0; i < array.length; i += size) out.push(array.slice(i, i + size));
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toSigned256(x) {
|
|
161
|
+
return x >= TWO_255 ? x - TWO_256 : x;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decodeAlgebraGlobalState(hexData) {
|
|
165
|
+
if (!hexData || hexData === '0x') {
|
|
166
|
+
throw new Error('Empty globalState response');
|
|
167
|
+
}
|
|
168
|
+
const clean = hexData.slice(2);
|
|
169
|
+
const words = [];
|
|
170
|
+
for (let i = 0; i < clean.length; i += 64) {
|
|
171
|
+
const chunk = clean.slice(i, i + 64);
|
|
172
|
+
if (chunk.length === 0) break;
|
|
173
|
+
words.push(BigInt('0x' + chunk));
|
|
174
|
+
}
|
|
175
|
+
if (words.length < 2) {
|
|
176
|
+
throw new Error('Unexpected globalState payload');
|
|
177
|
+
}
|
|
178
|
+
const sqrtPriceX96 = words[0];
|
|
179
|
+
const tickBig = toSigned256(words[1]);
|
|
180
|
+
const feeWord = words[2] ?? 0n;
|
|
181
|
+
const tick = Number(tickBig);
|
|
182
|
+
if (!Number.isSafeInteger(tick)) {
|
|
183
|
+
throw new Error('Decoded Algebra tick is not a safe integer');
|
|
184
|
+
}
|
|
185
|
+
const feePips = Number(feeWord);
|
|
186
|
+
return { sqrtPriceX96, tick, feePips };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function percentToTickDelta(pct) {
|
|
190
|
+
const r = 1 + pct / 100;
|
|
191
|
+
const delta = Math.log(r) / Math.log(1.0001);
|
|
192
|
+
return Math.max(1, Math.round(delta));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function floorDiv(a, b) {
|
|
196
|
+
const q = Math.trunc(a / b);
|
|
197
|
+
const r = a % b;
|
|
198
|
+
return (r !== 0 && ((r > 0) !== (b > 0))) ? q - 1 : q;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function fetchPoolSnapshotViem({ poolAddress, rpcUrl, percentBuckets = [1,2,5,10], client, blockTag = 'latest' }) {
|
|
202
|
+
let c = client;
|
|
203
|
+
if (!c) {
|
|
204
|
+
c = createPublicClient({ transport: http(rpcUrl) });
|
|
205
|
+
}
|
|
206
|
+
const pool = getAddress(poolAddress);
|
|
207
|
+
const readBlockParams =
|
|
208
|
+
typeof blockTag === 'bigint'
|
|
209
|
+
? { blockNumber: blockTag }
|
|
210
|
+
: typeof blockTag === 'number'
|
|
211
|
+
? { blockNumber: BigInt(blockTag) }
|
|
212
|
+
: blockTag && blockTag !== 'latest'
|
|
213
|
+
? { blockTag }
|
|
214
|
+
: {};
|
|
215
|
+
|
|
216
|
+
let resolvedBlockNumber;
|
|
217
|
+
let resolvedBlockTimestamp;
|
|
218
|
+
try {
|
|
219
|
+
const blk = await c.getBlock(
|
|
220
|
+
'blockNumber' in readBlockParams || 'blockTag' in readBlockParams
|
|
221
|
+
? readBlockParams
|
|
222
|
+
: { blockTag: 'latest' }
|
|
223
|
+
);
|
|
224
|
+
if (blk) {
|
|
225
|
+
resolvedBlockNumber = blk.number;
|
|
226
|
+
resolvedBlockTimestamp = blk.timestamp;
|
|
227
|
+
}
|
|
228
|
+
} catch (_) {
|
|
229
|
+
try {
|
|
230
|
+
resolvedBlockNumber = await c.getBlockNumber();
|
|
231
|
+
} catch (_) {}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let sqrtPriceX96;
|
|
235
|
+
let tick;
|
|
236
|
+
let liquidity;
|
|
237
|
+
let feePips;
|
|
238
|
+
let tickSpacing;
|
|
239
|
+
let token0Addr;
|
|
240
|
+
let token1Addr;
|
|
241
|
+
let poolType = 'uniswap';
|
|
242
|
+
const poolCacheKey = pool.toLowerCase();
|
|
243
|
+
const cachedPoolStatic = poolStaticCache.get(poolCacheKey);
|
|
244
|
+
|
|
245
|
+
if (cachedPoolStatic?.poolType === 'uniswap') {
|
|
246
|
+
poolType = 'uniswap';
|
|
247
|
+
const [slot0, L] = await readContracts({
|
|
248
|
+
client: c,
|
|
249
|
+
rpcUrl,
|
|
250
|
+
blockTag,
|
|
251
|
+
address: pool,
|
|
252
|
+
abi: UNISWAP_POOL_ABI,
|
|
253
|
+
calls: [{ functionName: 'slot0' }, { functionName: 'liquidity' }],
|
|
254
|
+
readBlockParams,
|
|
255
|
+
});
|
|
256
|
+
sqrtPriceX96 = slot0[0];
|
|
257
|
+
tick = Number(slot0[1]);
|
|
258
|
+
liquidity = L;
|
|
259
|
+
feePips = cachedPoolStatic.feePips;
|
|
260
|
+
tickSpacing = cachedPoolStatic.tickSpacing;
|
|
261
|
+
token0Addr = cachedPoolStatic.token0Addr;
|
|
262
|
+
token1Addr = cachedPoolStatic.token1Addr;
|
|
263
|
+
} else if (cachedPoolStatic?.poolType === 'algebra') {
|
|
264
|
+
poolType = 'algebra';
|
|
265
|
+
const [globalStateHex, L] = await Promise.all([
|
|
266
|
+
readRawCall({ client: c, rpcUrl, to: pool, data: GLOBAL_STATE_SELECTOR, blockTag }),
|
|
267
|
+
readContracts({
|
|
268
|
+
client: c,
|
|
269
|
+
rpcUrl,
|
|
270
|
+
blockTag,
|
|
271
|
+
address: pool,
|
|
272
|
+
abi: ALGEBRA_POOL_ABI,
|
|
273
|
+
calls: [{ functionName: 'liquidity' }],
|
|
274
|
+
readBlockParams,
|
|
275
|
+
}).then((results) => results[0]),
|
|
276
|
+
]);
|
|
277
|
+
const globalState = decodeAlgebraGlobalState(globalStateHex);
|
|
278
|
+
sqrtPriceX96 = globalState.sqrtPriceX96;
|
|
279
|
+
tick = globalState.tick;
|
|
280
|
+
liquidity = L;
|
|
281
|
+
feePips = globalState.feePips;
|
|
282
|
+
tickSpacing = cachedPoolStatic.tickSpacing;
|
|
283
|
+
token0Addr = cachedPoolStatic.token0Addr;
|
|
284
|
+
token1Addr = cachedPoolStatic.token1Addr;
|
|
285
|
+
} else {
|
|
286
|
+
try {
|
|
287
|
+
const [slot0, L, fee, spacing, tok0, tok1] = await readContracts({
|
|
288
|
+
client: c,
|
|
289
|
+
rpcUrl,
|
|
290
|
+
blockTag,
|
|
291
|
+
address: pool,
|
|
292
|
+
abi: UNISWAP_POOL_ABI,
|
|
293
|
+
calls: [
|
|
294
|
+
{ functionName: 'slot0' },
|
|
295
|
+
{ functionName: 'liquidity' },
|
|
296
|
+
{ functionName: 'fee' },
|
|
297
|
+
{ functionName: 'tickSpacing' },
|
|
298
|
+
{ functionName: 'token0' },
|
|
299
|
+
{ functionName: 'token1' },
|
|
300
|
+
],
|
|
301
|
+
readBlockParams,
|
|
302
|
+
});
|
|
303
|
+
sqrtPriceX96 = slot0[0];
|
|
304
|
+
tick = Number(slot0[1]);
|
|
305
|
+
liquidity = L;
|
|
306
|
+
feePips = Number(fee);
|
|
307
|
+
tickSpacing = Number(spacing);
|
|
308
|
+
token0Addr = tok0;
|
|
309
|
+
token1Addr = tok1;
|
|
310
|
+
poolStaticCache.set(poolCacheKey, {
|
|
311
|
+
poolType: 'uniswap',
|
|
312
|
+
feePips,
|
|
313
|
+
tickSpacing,
|
|
314
|
+
token0Addr,
|
|
315
|
+
token1Addr,
|
|
316
|
+
});
|
|
317
|
+
} catch (uniswapErr) {
|
|
318
|
+
poolType = 'algebra';
|
|
319
|
+
let globalState;
|
|
320
|
+
try {
|
|
321
|
+
const callResult = await readRawCall({
|
|
322
|
+
client: c,
|
|
323
|
+
rpcUrl,
|
|
324
|
+
to: pool,
|
|
325
|
+
data: GLOBAL_STATE_SELECTOR,
|
|
326
|
+
blockTag,
|
|
327
|
+
});
|
|
328
|
+
globalState = decodeAlgebraGlobalState(callResult);
|
|
329
|
+
} catch (algebraStateErr) {
|
|
330
|
+
const combined = new Error(`Failed to read pool state as Uniswap (slot0): ${uniswapErr?.message || uniswapErr}. Algebra globalState call also failed: ${algebraStateErr?.message || algebraStateErr}`);
|
|
331
|
+
combined.cause = { uniswap: uniswapErr, algebra: algebraStateErr };
|
|
332
|
+
throw combined;
|
|
333
|
+
}
|
|
334
|
+
const [L, spacing, tok0, tok1] = await readContracts({
|
|
335
|
+
client: c,
|
|
336
|
+
rpcUrl,
|
|
337
|
+
blockTag,
|
|
338
|
+
address: pool,
|
|
339
|
+
abi: ALGEBRA_POOL_ABI,
|
|
340
|
+
calls: [
|
|
341
|
+
{ functionName: 'liquidity' },
|
|
342
|
+
{ functionName: 'tickSpacing' },
|
|
343
|
+
{ functionName: 'token0' },
|
|
344
|
+
{ functionName: 'token1' },
|
|
345
|
+
],
|
|
346
|
+
readBlockParams,
|
|
347
|
+
});
|
|
348
|
+
sqrtPriceX96 = globalState.sqrtPriceX96;
|
|
349
|
+
tick = globalState.tick;
|
|
350
|
+
liquidity = L;
|
|
351
|
+
feePips = globalState.feePips;
|
|
352
|
+
tickSpacing = Number(spacing);
|
|
353
|
+
token0Addr = tok0;
|
|
354
|
+
token1Addr = tok1;
|
|
355
|
+
poolStaticCache.set(poolCacheKey, {
|
|
356
|
+
poolType: 'algebra',
|
|
357
|
+
tickSpacing,
|
|
358
|
+
token0Addr,
|
|
359
|
+
token1Addr,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
tickSpacing = Math.abs(Number(tickSpacing));
|
|
365
|
+
if (!tickSpacing) {
|
|
366
|
+
throw new Error('tickSpacing resolved to zero');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const token0Key = token0Addr.toLowerCase();
|
|
370
|
+
const token1Key = token1Addr.toLowerCase();
|
|
371
|
+
let token0Meta = tokenMetaCache.get(token0Key);
|
|
372
|
+
let token1Meta = tokenMetaCache.get(token1Key);
|
|
373
|
+
|
|
374
|
+
if (!token0Meta || !token1Meta) {
|
|
375
|
+
if (rpcUrl) {
|
|
376
|
+
const pendingCalls = [];
|
|
377
|
+
if (!token0Meta) {
|
|
378
|
+
pendingCalls.push(
|
|
379
|
+
{ address: token0Addr, abi: ERC20_ABI, functionName: 'decimals' },
|
|
380
|
+
{ address: token0Addr, abi: ERC20_ABI, functionName: 'symbol' }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (!token1Meta) {
|
|
384
|
+
pendingCalls.push(
|
|
385
|
+
{ address: token1Addr, abi: ERC20_ABI, functionName: 'decimals' },
|
|
386
|
+
{ address: token1Addr, abi: ERC20_ABI, functionName: 'symbol' }
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const results = await readContractCallsBatch({ rpcUrl, blockTag, calls: pendingCalls });
|
|
390
|
+
let offset = 0;
|
|
391
|
+
if (!token0Meta) {
|
|
392
|
+
token0Meta = { decimals: Number(results[offset]), symbol: results[offset + 1] };
|
|
393
|
+
tokenMetaCache.set(token0Key, token0Meta);
|
|
394
|
+
offset += 2;
|
|
395
|
+
}
|
|
396
|
+
if (!token1Meta) {
|
|
397
|
+
token1Meta = { decimals: Number(results[offset]), symbol: results[offset + 1] };
|
|
398
|
+
tokenMetaCache.set(token1Key, token1Meta);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
if (!token0Meta) {
|
|
402
|
+
const [decimals, symbol] = await readContracts({
|
|
403
|
+
client: c,
|
|
404
|
+
rpcUrl,
|
|
405
|
+
blockTag,
|
|
406
|
+
address: token0Addr,
|
|
407
|
+
abi: ERC20_ABI,
|
|
408
|
+
calls: [{ functionName: 'decimals' }, { functionName: 'symbol' }],
|
|
409
|
+
readBlockParams,
|
|
410
|
+
});
|
|
411
|
+
token0Meta = { decimals: Number(decimals), symbol };
|
|
412
|
+
tokenMetaCache.set(token0Key, token0Meta);
|
|
413
|
+
}
|
|
414
|
+
if (!token1Meta) {
|
|
415
|
+
const [decimals, symbol] = await readContracts({
|
|
416
|
+
client: c,
|
|
417
|
+
rpcUrl,
|
|
418
|
+
blockTag,
|
|
419
|
+
address: token1Addr,
|
|
420
|
+
abi: ERC20_ABI,
|
|
421
|
+
calls: [{ functionName: 'decimals' }, { functionName: 'symbol' }],
|
|
422
|
+
readBlockParams,
|
|
423
|
+
});
|
|
424
|
+
token1Meta = { decimals: Number(decimals), symbol };
|
|
425
|
+
tokenMetaCache.set(token1Key, token1Meta);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const far = Math.max(...percentBuckets);
|
|
431
|
+
const delta = percentToTickDelta(far);
|
|
432
|
+
const start = tick - delta;
|
|
433
|
+
const end = tick + delta;
|
|
434
|
+
|
|
435
|
+
const compStart = floorDiv(start, tickSpacing);
|
|
436
|
+
const compEnd = floorDiv(end, tickSpacing);
|
|
437
|
+
const wordStart = floorDiv(compStart, 256);
|
|
438
|
+
const wordEnd = floorDiv(compEnd, 256);
|
|
439
|
+
|
|
440
|
+
const ticks = [];
|
|
441
|
+
const poolAbi = poolType === 'algebra' ? ALGEBRA_POOL_ABI : UNISWAP_POOL_ABI;
|
|
442
|
+
const bitmapFn = poolType === 'algebra' ? 'tickTable' : 'tickBitmap';
|
|
443
|
+
const bitmapWords = [];
|
|
444
|
+
for (let w = wordStart; w <= wordEnd; w++) bitmapWords.push(w);
|
|
445
|
+
const bitmapResults = bitmapWords.length
|
|
446
|
+
? await readContracts({
|
|
447
|
+
client: c,
|
|
448
|
+
rpcUrl,
|
|
449
|
+
blockTag,
|
|
450
|
+
address: pool,
|
|
451
|
+
abi: poolAbi,
|
|
452
|
+
calls: bitmapWords.map((w) => ({ functionName: bitmapFn, args: [BigInt(w)] })),
|
|
453
|
+
readBlockParams,
|
|
454
|
+
})
|
|
455
|
+
: [];
|
|
456
|
+
const tickIndexes = [];
|
|
457
|
+
for (let i = 0; i < bitmapWords.length; i++) {
|
|
458
|
+
const w = bitmapWords[i];
|
|
459
|
+
const bitmap = bitmapResults[i];
|
|
460
|
+
if (bitmap === 0n) continue;
|
|
461
|
+
for (let bit = 0; bit < 256; bit++) {
|
|
462
|
+
if (((bitmap >> BigInt(bit)) & 1n) === 0n) continue;
|
|
463
|
+
const compressed = w * 256 + bit;
|
|
464
|
+
const t = compressed * tickSpacing;
|
|
465
|
+
if (t < start || t > end) continue;
|
|
466
|
+
tickIndexes.push(t);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (const tickBatch of chunk(tickIndexes, 128)) {
|
|
471
|
+
const tickResults = await readContracts({
|
|
472
|
+
client: c,
|
|
473
|
+
rpcUrl,
|
|
474
|
+
blockTag,
|
|
475
|
+
address: pool,
|
|
476
|
+
abi: poolAbi,
|
|
477
|
+
calls: tickBatch.map((tickIndex) => ({ functionName: 'ticks', args: [tickIndex] })),
|
|
478
|
+
readBlockParams,
|
|
479
|
+
});
|
|
480
|
+
for (let i = 0; i < tickBatch.length; i++) {
|
|
481
|
+
ticks.push({ index: tickBatch[i], liquidityNet: tickResults[i][1] });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
sqrtPriceX96,
|
|
487
|
+
tick,
|
|
488
|
+
liquidity,
|
|
489
|
+
feePips,
|
|
490
|
+
ticks: ticks.sort((a,b) => a.index - b.index),
|
|
491
|
+
token0: { decimals: token0Meta.decimals, usdPrice: 0, symbol: token0Meta.symbol, id: token0Addr },
|
|
492
|
+
token1: { decimals: token1Meta.decimals, usdPrice: 0, symbol: token1Meta.symbol, id: token1Addr },
|
|
493
|
+
meta: {
|
|
494
|
+
token0: { decimals: token0Meta.decimals, symbol: token0Meta.symbol, id: token0Addr },
|
|
495
|
+
token1: { decimals: token1Meta.decimals, symbol: token1Meta.symbol, id: token1Addr },
|
|
496
|
+
poolId: pool,
|
|
497
|
+
range: { start, end },
|
|
498
|
+
farPercent: far,
|
|
499
|
+
tickSpacing,
|
|
500
|
+
poolType,
|
|
501
|
+
blockNumber: resolvedBlockNumber,
|
|
502
|
+
blockTimestamp: resolvedBlockTimestamp,
|
|
503
|
+
blockTag,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
}
|