@steerprotocol/liquidity-meter 1.0.0 → 2.0.0
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 +18 -56
- package/bin/liquidity-depth.js +887 -0
- package/package.json +23 -32
- package/src/api.js +490 -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 +233 -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,233 @@
|
|
|
1
|
+
// On-chain fetcher for Uniswap v3 pools using a viem public client
|
|
2
|
+
// - Reads pool slot0, liquidity, fee, tickSpacing, token0/1 addresses + ERC20 decimals/symbols
|
|
3
|
+
// - Enumerates initialized ticks via tickBitmap within a bounded range
|
|
4
|
+
|
|
5
|
+
import { createPublicClient, getAddress, http, parseAbi } from 'viem';
|
|
6
|
+
|
|
7
|
+
const UNISWAP_POOL_ABI = parseAbi([
|
|
8
|
+
'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
|
|
9
|
+
'function liquidity() view returns (uint128)',
|
|
10
|
+
'function fee() view returns (uint24)',
|
|
11
|
+
'function tickSpacing() view returns (int24)',
|
|
12
|
+
'function token0() view returns (address)',
|
|
13
|
+
'function token1() view returns (address)',
|
|
14
|
+
'function tickBitmap(int16) view returns (uint256)',
|
|
15
|
+
'function ticks(int24 tick) view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized)'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const ALGEBRA_POOL_ABI = parseAbi([
|
|
19
|
+
'function liquidity() view returns (uint128)',
|
|
20
|
+
'function tickSpacing() view returns (int24)',
|
|
21
|
+
'function token0() view returns (address)',
|
|
22
|
+
'function token1() view returns (address)',
|
|
23
|
+
'function tickTable(int16) view returns (uint256)',
|
|
24
|
+
'function ticks(int24 tick) view returns (uint256 liquidityTotal, int128 liquidityDelta, int24 prevTick, int24 nextTick, uint256 outerFeeGrowth0Token, uint256 outerFeeGrowth1Token)'
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const ERC20_ABI = parseAbi([
|
|
28
|
+
'function decimals() view returns (uint8)',
|
|
29
|
+
'function symbol() view returns (string)'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const GLOBAL_STATE_SELECTOR = '0xe76c01e4';
|
|
33
|
+
const TWO_256 = 1n << 256n;
|
|
34
|
+
const TWO_255 = 1n << 255n;
|
|
35
|
+
|
|
36
|
+
function toSigned256(x) {
|
|
37
|
+
return x >= TWO_255 ? x - TWO_256 : x;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decodeAlgebraGlobalState(hexData) {
|
|
41
|
+
if (!hexData || hexData === '0x') {
|
|
42
|
+
throw new Error('Empty globalState response');
|
|
43
|
+
}
|
|
44
|
+
const clean = hexData.slice(2);
|
|
45
|
+
const words = [];
|
|
46
|
+
for (let i = 0; i < clean.length; i += 64) {
|
|
47
|
+
const chunk = clean.slice(i, i + 64);
|
|
48
|
+
if (chunk.length === 0) break;
|
|
49
|
+
words.push(BigInt('0x' + chunk));
|
|
50
|
+
}
|
|
51
|
+
if (words.length < 2) {
|
|
52
|
+
throw new Error('Unexpected globalState payload');
|
|
53
|
+
}
|
|
54
|
+
const sqrtPriceX96 = words[0];
|
|
55
|
+
const tickBig = toSigned256(words[1]);
|
|
56
|
+
const feeWord = words[2] ?? 0n;
|
|
57
|
+
const tick = Number(tickBig);
|
|
58
|
+
if (!Number.isSafeInteger(tick)) {
|
|
59
|
+
throw new Error('Decoded Algebra tick is not a safe integer');
|
|
60
|
+
}
|
|
61
|
+
const feePips = Number(feeWord);
|
|
62
|
+
return { sqrtPriceX96, tick, feePips };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function percentToTickDelta(pct) {
|
|
66
|
+
const r = 1 + pct / 100;
|
|
67
|
+
const delta = Math.log(r) / Math.log(1.0001);
|
|
68
|
+
return Math.max(1, Math.round(delta));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function floorDiv(a, b) {
|
|
72
|
+
const q = Math.trunc(a / b);
|
|
73
|
+
const r = a % b;
|
|
74
|
+
return (r !== 0 && ((r > 0) !== (b > 0))) ? q - 1 : q;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function fetchPoolSnapshotViem({ poolAddress, rpcUrl, percentBuckets = [1,2,5,10], client, blockTag = 'latest' }) {
|
|
78
|
+
let c = client;
|
|
79
|
+
if (!c) {
|
|
80
|
+
c = createPublicClient({ transport: http(rpcUrl) });
|
|
81
|
+
}
|
|
82
|
+
const pool = getAddress(poolAddress);
|
|
83
|
+
const readBlockParams =
|
|
84
|
+
typeof blockTag === 'bigint'
|
|
85
|
+
? { blockNumber: blockTag }
|
|
86
|
+
: typeof blockTag === 'number'
|
|
87
|
+
? { blockNumber: BigInt(blockTag) }
|
|
88
|
+
: blockTag && blockTag !== 'latest'
|
|
89
|
+
? { blockTag }
|
|
90
|
+
: {};
|
|
91
|
+
|
|
92
|
+
let resolvedBlockNumber;
|
|
93
|
+
let resolvedBlockTimestamp;
|
|
94
|
+
try {
|
|
95
|
+
const blk = await c.getBlock(
|
|
96
|
+
'blockNumber' in readBlockParams || 'blockTag' in readBlockParams
|
|
97
|
+
? readBlockParams
|
|
98
|
+
: { blockTag: 'latest' }
|
|
99
|
+
);
|
|
100
|
+
if (blk) {
|
|
101
|
+
resolvedBlockNumber = blk.number;
|
|
102
|
+
resolvedBlockTimestamp = blk.timestamp;
|
|
103
|
+
}
|
|
104
|
+
} catch (_) {
|
|
105
|
+
try {
|
|
106
|
+
resolvedBlockNumber = await c.getBlockNumber();
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let sqrtPriceX96;
|
|
111
|
+
let tick;
|
|
112
|
+
let liquidity;
|
|
113
|
+
let feePips;
|
|
114
|
+
let tickSpacing;
|
|
115
|
+
let token0Addr;
|
|
116
|
+
let token1Addr;
|
|
117
|
+
let poolType = 'uniswap';
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const [slot0, L, fee, spacing, tok0, tok1] = await Promise.all([
|
|
121
|
+
c.readContract({ address: pool, abi: UNISWAP_POOL_ABI, functionName: 'slot0', ...readBlockParams }),
|
|
122
|
+
c.readContract({ address: pool, abi: UNISWAP_POOL_ABI, functionName: 'liquidity', ...readBlockParams }),
|
|
123
|
+
c.readContract({ address: pool, abi: UNISWAP_POOL_ABI, functionName: 'fee', ...readBlockParams }),
|
|
124
|
+
c.readContract({ address: pool, abi: UNISWAP_POOL_ABI, functionName: 'tickSpacing', ...readBlockParams }),
|
|
125
|
+
c.readContract({ address: pool, abi: UNISWAP_POOL_ABI, functionName: 'token0', ...readBlockParams }),
|
|
126
|
+
c.readContract({ address: pool, abi: UNISWAP_POOL_ABI, functionName: 'token1', ...readBlockParams }),
|
|
127
|
+
]);
|
|
128
|
+
sqrtPriceX96 = slot0[0];
|
|
129
|
+
tick = Number(slot0[1]);
|
|
130
|
+
liquidity = L;
|
|
131
|
+
feePips = Number(fee);
|
|
132
|
+
tickSpacing = Number(spacing);
|
|
133
|
+
token0Addr = tok0;
|
|
134
|
+
token1Addr = tok1;
|
|
135
|
+
} catch (uniswapErr) {
|
|
136
|
+
poolType = 'algebra';
|
|
137
|
+
let globalState;
|
|
138
|
+
try {
|
|
139
|
+
const callResult = await c.transport.request({
|
|
140
|
+
method: 'eth_call',
|
|
141
|
+
params: [
|
|
142
|
+
{ to: pool, data: GLOBAL_STATE_SELECTOR },
|
|
143
|
+
typeof blockTag === 'bigint'
|
|
144
|
+
? `0x${blockTag.toString(16)}`
|
|
145
|
+
: typeof blockTag === 'number'
|
|
146
|
+
? `0x${blockTag.toString(16)}`
|
|
147
|
+
: blockTag ?? 'latest',
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
globalState = decodeAlgebraGlobalState(callResult);
|
|
151
|
+
} catch (algebraStateErr) {
|
|
152
|
+
const combined = new Error(`Failed to read pool state as Uniswap (slot0): ${uniswapErr?.message || uniswapErr}. Algebra globalState call also failed: ${algebraStateErr?.message || algebraStateErr}`);
|
|
153
|
+
combined.cause = { uniswap: uniswapErr, algebra: algebraStateErr };
|
|
154
|
+
throw combined;
|
|
155
|
+
}
|
|
156
|
+
const [L, spacing, tok0, tok1] = await Promise.all([
|
|
157
|
+
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'liquidity', ...readBlockParams }),
|
|
158
|
+
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'tickSpacing', ...readBlockParams }),
|
|
159
|
+
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'token0', ...readBlockParams }),
|
|
160
|
+
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'token1', ...readBlockParams }),
|
|
161
|
+
]);
|
|
162
|
+
sqrtPriceX96 = globalState.sqrtPriceX96;
|
|
163
|
+
tick = globalState.tick;
|
|
164
|
+
liquidity = L;
|
|
165
|
+
feePips = globalState.feePips;
|
|
166
|
+
tickSpacing = Number(spacing);
|
|
167
|
+
token0Addr = tok0;
|
|
168
|
+
token1Addr = tok1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
tickSpacing = Math.abs(Number(tickSpacing));
|
|
172
|
+
if (!tickSpacing) {
|
|
173
|
+
throw new Error('tickSpacing resolved to zero');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const [dec0, sym0, dec1, sym1] = await Promise.all([
|
|
177
|
+
c.readContract({ address: token0Addr, abi: ERC20_ABI, functionName: 'decimals', ...readBlockParams }),
|
|
178
|
+
c.readContract({ address: token0Addr, abi: ERC20_ABI, functionName: 'symbol', ...readBlockParams }),
|
|
179
|
+
c.readContract({ address: token1Addr, abi: ERC20_ABI, functionName: 'decimals', ...readBlockParams }),
|
|
180
|
+
c.readContract({ address: token1Addr, abi: ERC20_ABI, functionName: 'symbol', ...readBlockParams }),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const far = Math.max(...percentBuckets);
|
|
184
|
+
const delta = percentToTickDelta(far);
|
|
185
|
+
const start = tick - delta;
|
|
186
|
+
const end = tick + delta;
|
|
187
|
+
|
|
188
|
+
const compStart = floorDiv(start, tickSpacing);
|
|
189
|
+
const compEnd = floorDiv(end, tickSpacing);
|
|
190
|
+
const wordStart = floorDiv(compStart, 256);
|
|
191
|
+
const wordEnd = floorDiv(compEnd, 256);
|
|
192
|
+
|
|
193
|
+
const ticks = [];
|
|
194
|
+
const poolAbi = poolType === 'algebra' ? ALGEBRA_POOL_ABI : UNISWAP_POOL_ABI;
|
|
195
|
+
const bitmapFn = poolType === 'algebra' ? 'tickTable' : 'tickBitmap';
|
|
196
|
+
for (let w = wordStart; w <= wordEnd; w++) {
|
|
197
|
+
// tickBitmap index is int16
|
|
198
|
+
const bitmap = await c.readContract({ address: pool, abi: poolAbi, functionName: bitmapFn, args: [BigInt(w)], ...readBlockParams });
|
|
199
|
+
if (bitmap === 0n) continue;
|
|
200
|
+
for (let bit = 0; bit < 256; bit++) {
|
|
201
|
+
if (((bitmap >> BigInt(bit)) & 1n) === 0n) continue;
|
|
202
|
+
const compressed = w * 256 + bit;
|
|
203
|
+
const t = compressed * tickSpacing;
|
|
204
|
+
if (t < start || t > end) continue;
|
|
205
|
+
// Read tick data to get liquidityNet
|
|
206
|
+
const ret = await c.readContract({ address: pool, abi: poolAbi, functionName: 'ticks', args: [t], ...readBlockParams });
|
|
207
|
+
const liquidityNet = ret[1];
|
|
208
|
+
ticks.push({ index: t, liquidityNet: liquidityNet });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
sqrtPriceX96,
|
|
214
|
+
tick,
|
|
215
|
+
liquidity,
|
|
216
|
+
feePips,
|
|
217
|
+
ticks: ticks.sort((a,b) => a.index - b.index),
|
|
218
|
+
token0: { decimals: Number(dec0), usdPrice: 0, symbol: sym0, id: token0Addr },
|
|
219
|
+
token1: { decimals: Number(dec1), usdPrice: 0, symbol: sym1, id: token1Addr },
|
|
220
|
+
meta: {
|
|
221
|
+
token0: { decimals: Number(dec0), symbol: sym0, id: token0Addr },
|
|
222
|
+
token1: { decimals: Number(dec1), symbol: sym1, id: token1Addr },
|
|
223
|
+
poolId: pool,
|
|
224
|
+
range: { start, end },
|
|
225
|
+
farPercent: far,
|
|
226
|
+
tickSpacing,
|
|
227
|
+
poolType,
|
|
228
|
+
blockNumber: resolvedBlockNumber,
|
|
229
|
+
blockTimestamp: resolvedBlockTimestamp,
|
|
230
|
+
blockTag,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
package/src/withdraw.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPublicClient,
|
|
3
|
+
encodeFunctionData,
|
|
4
|
+
getAddress,
|
|
5
|
+
http as viemHttp,
|
|
6
|
+
parseAbi,
|
|
7
|
+
parseEther,
|
|
8
|
+
} from 'viem';
|
|
9
|
+
import { sendTransaction as viemSendTransaction } from 'viem/actions';
|
|
10
|
+
|
|
11
|
+
const ERC20_ABI = parseAbi([
|
|
12
|
+
'function balanceOf(address) view returns (uint256)',
|
|
13
|
+
'function decimals() view returns (uint8)',
|
|
14
|
+
'function symbol() view returns (string)',
|
|
15
|
+
'function totalSupply() view returns (uint256)',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const VAULT_ABI = parseAbi([
|
|
19
|
+
'function pool() view returns (address)',
|
|
20
|
+
'function token0() view returns (address)',
|
|
21
|
+
'function token1() view returns (address)',
|
|
22
|
+
'function decimals() view returns (uint8)',
|
|
23
|
+
'function withdraw(uint256 shares,uint256 amount0Min,uint256 amount1Min,address to) returns (uint256 amount0,uint256 amount1)',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function formatUnits(value, decimals) {
|
|
27
|
+
try {
|
|
28
|
+
let normalized = BigInt(value);
|
|
29
|
+
const negative = normalized < 0n;
|
|
30
|
+
if (negative) normalized = -normalized;
|
|
31
|
+
const stringified = normalized
|
|
32
|
+
.toString()
|
|
33
|
+
.padStart(Number(decimals) + 1, '0');
|
|
34
|
+
const integer = stringified.slice(0, stringified.length - Number(decimals));
|
|
35
|
+
const fraction = stringified
|
|
36
|
+
.slice(stringified.length - Number(decimals))
|
|
37
|
+
.replace(/0+$/, '')
|
|
38
|
+
.slice(0, 8);
|
|
39
|
+
return `${negative ? '-' : ''}${integer}${fraction ? `.${fraction}` : ''}`;
|
|
40
|
+
} catch (_) {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatToken(value, decimals, symbol) {
|
|
46
|
+
return `${formatUnits(value, decimals)} ${symbol || ''}`.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function createForkClient({ rpcUrl, blockTag = 'latest' }) {
|
|
50
|
+
const { createMemoryClient, http } = await import('tevm');
|
|
51
|
+
const client = createMemoryClient({
|
|
52
|
+
fork: { transport: http(rpcUrl), blockTag },
|
|
53
|
+
loggingLevel: 'error',
|
|
54
|
+
});
|
|
55
|
+
if (client.tevmReady) await client.tevmReady();
|
|
56
|
+
return client;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function resolveTevmCommon(rpcUrl) {
|
|
60
|
+
try {
|
|
61
|
+
const tevmCommon = await import('tevm/common');
|
|
62
|
+
const client = createPublicClient({ transport: viemHttp(rpcUrl) });
|
|
63
|
+
const chainId = await client.getChainId();
|
|
64
|
+
for (const value of Object.values(tevmCommon)) {
|
|
65
|
+
if (value && typeof value === 'object' && 'id' in value && value.id === chainId) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (_) {}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function resolveViemChain(rpcUrl) {
|
|
74
|
+
try {
|
|
75
|
+
const viemChains = await import('viem/chains');
|
|
76
|
+
const client = createPublicClient({ transport: viemHttp(rpcUrl) });
|
|
77
|
+
const chainId = await client.getChainId();
|
|
78
|
+
for (const value of Object.values(viemChains)) {
|
|
79
|
+
if (value && typeof value === 'object' && 'id' in value && value.id === chainId) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (_) {}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function resolveViemChainFromClient(client) {
|
|
88
|
+
try {
|
|
89
|
+
const viemChains = await import('viem/chains');
|
|
90
|
+
const chainId = await client.getChainId();
|
|
91
|
+
for (const value of Object.values(viemChains)) {
|
|
92
|
+
if (value && typeof value === 'object' && 'id' in value && value.id === chainId) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function simulateVaultWithdraw({
|
|
101
|
+
client,
|
|
102
|
+
vault,
|
|
103
|
+
owner,
|
|
104
|
+
withdrawPct,
|
|
105
|
+
withdrawShares,
|
|
106
|
+
debug = false,
|
|
107
|
+
cliPool,
|
|
108
|
+
viemChain,
|
|
109
|
+
sendTransaction = viemSendTransaction,
|
|
110
|
+
}) {
|
|
111
|
+
if (!client) throw new Error('simulateVaultWithdraw requires tevm client');
|
|
112
|
+
if (!vault) throw new Error('Missing --vault');
|
|
113
|
+
if (!owner) throw new Error('Missing --owner for withdraw simulation');
|
|
114
|
+
|
|
115
|
+
const vaultAddress = getAddress(vault);
|
|
116
|
+
const ownerAddress = getAddress(owner);
|
|
117
|
+
|
|
118
|
+
let poolAddress;
|
|
119
|
+
try {
|
|
120
|
+
poolAddress = await client.readContract({
|
|
121
|
+
address: vaultAddress,
|
|
122
|
+
abi: VAULT_ABI,
|
|
123
|
+
functionName: 'pool',
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Failed to read vault.pool(): ${error?.message || error}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let vaultDecimals = 18;
|
|
130
|
+
try {
|
|
131
|
+
vaultDecimals = Number(
|
|
132
|
+
await client.readContract({
|
|
133
|
+
address: vaultAddress,
|
|
134
|
+
abi: VAULT_ABI,
|
|
135
|
+
functionName: 'decimals',
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
|
|
140
|
+
const token0Address = await client.readContract({
|
|
141
|
+
address: vaultAddress,
|
|
142
|
+
abi: VAULT_ABI,
|
|
143
|
+
functionName: 'token0',
|
|
144
|
+
});
|
|
145
|
+
const token1Address = await client.readContract({
|
|
146
|
+
address: vaultAddress,
|
|
147
|
+
abi: VAULT_ABI,
|
|
148
|
+
functionName: 'token1',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const [token0Decimals, token1Decimals] = await Promise.all([
|
|
152
|
+
client.readContract({
|
|
153
|
+
address: token0Address,
|
|
154
|
+
abi: ERC20_ABI,
|
|
155
|
+
functionName: 'decimals',
|
|
156
|
+
}),
|
|
157
|
+
client.readContract({
|
|
158
|
+
address: token1Address,
|
|
159
|
+
abi: ERC20_ABI,
|
|
160
|
+
functionName: 'decimals',
|
|
161
|
+
}),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
let [token0Symbol, token1Symbol] = ['T0', 'T1'];
|
|
165
|
+
try {
|
|
166
|
+
token0Symbol = await client.readContract({
|
|
167
|
+
address: token0Address,
|
|
168
|
+
abi: ERC20_ABI,
|
|
169
|
+
functionName: 'symbol',
|
|
170
|
+
});
|
|
171
|
+
} catch (_) {}
|
|
172
|
+
try {
|
|
173
|
+
token1Symbol = await client.readContract({
|
|
174
|
+
address: token1Address,
|
|
175
|
+
abi: ERC20_ABI,
|
|
176
|
+
functionName: 'symbol',
|
|
177
|
+
});
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
|
|
180
|
+
let shares = null;
|
|
181
|
+
if (withdrawShares != null) {
|
|
182
|
+
try {
|
|
183
|
+
shares = BigInt(String(withdrawShares));
|
|
184
|
+
} catch (_) {
|
|
185
|
+
throw new Error('--withdraw-shares must be a BigInt-compatible value');
|
|
186
|
+
}
|
|
187
|
+
} else if (Number.isFinite(withdrawPct)) {
|
|
188
|
+
const balance = await client.readContract({
|
|
189
|
+
address: vaultAddress,
|
|
190
|
+
abi: ERC20_ABI,
|
|
191
|
+
functionName: 'balanceOf',
|
|
192
|
+
args: [ownerAddress],
|
|
193
|
+
});
|
|
194
|
+
const pct = BigInt(Math.max(0, Math.min(100, Math.floor(Number(withdrawPct)))));
|
|
195
|
+
shares = (BigInt(balance) * pct) / 100n;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ownerShares = await client.readContract({
|
|
199
|
+
address: vaultAddress,
|
|
200
|
+
abi: ERC20_ABI,
|
|
201
|
+
functionName: 'balanceOf',
|
|
202
|
+
args: [ownerAddress],
|
|
203
|
+
});
|
|
204
|
+
const totalShares = await client.readContract({
|
|
205
|
+
address: vaultAddress,
|
|
206
|
+
abi: ERC20_ABI,
|
|
207
|
+
functionName: 'totalSupply',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (debug) {
|
|
211
|
+
const poolWarning =
|
|
212
|
+
cliPool && cliPool.toLowerCase() !== poolAddress.toLowerCase()
|
|
213
|
+
? ` (CLI pool=${cliPool})`
|
|
214
|
+
: '';
|
|
215
|
+
console.error(
|
|
216
|
+
`[withdraw] vault=${vaultAddress} pool=${poolAddress}${poolWarning}`
|
|
217
|
+
);
|
|
218
|
+
console.error(
|
|
219
|
+
`[withdraw] owner=${ownerAddress} ownerShares=${formatToken(ownerShares, vaultDecimals, 'shares')} totalSupply=${formatToken(totalShares, vaultDecimals, 'shares')}`
|
|
220
|
+
);
|
|
221
|
+
console.error(
|
|
222
|
+
`[withdraw] planned shares=${formatToken(shares ?? 0n, vaultDecimals, 'shares')} (${Number(withdrawPct ?? 0)}%)`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!shares || shares <= 0n) {
|
|
227
|
+
if (debug) console.error('[withdraw] no shares to withdraw; skipping');
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await client.setBalance({ address: ownerAddress, value: parseEther('0.05') });
|
|
233
|
+
} catch (_) {}
|
|
234
|
+
try {
|
|
235
|
+
await client.impersonateAccount({ address: ownerAddress });
|
|
236
|
+
} catch (_) {}
|
|
237
|
+
|
|
238
|
+
const [owner0Before, owner1Before, vault0Before, vault1Before] = await Promise.all([
|
|
239
|
+
client.readContract({
|
|
240
|
+
address: token0Address,
|
|
241
|
+
abi: ERC20_ABI,
|
|
242
|
+
functionName: 'balanceOf',
|
|
243
|
+
args: [ownerAddress],
|
|
244
|
+
}),
|
|
245
|
+
client.readContract({
|
|
246
|
+
address: token1Address,
|
|
247
|
+
abi: ERC20_ABI,
|
|
248
|
+
functionName: 'balanceOf',
|
|
249
|
+
args: [ownerAddress],
|
|
250
|
+
}),
|
|
251
|
+
client.readContract({
|
|
252
|
+
address: token0Address,
|
|
253
|
+
abi: ERC20_ABI,
|
|
254
|
+
functionName: 'balanceOf',
|
|
255
|
+
args: [vaultAddress],
|
|
256
|
+
}),
|
|
257
|
+
client.readContract({
|
|
258
|
+
address: token1Address,
|
|
259
|
+
abi: ERC20_ABI,
|
|
260
|
+
functionName: 'balanceOf',
|
|
261
|
+
args: [vaultAddress],
|
|
262
|
+
}),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
if (debug) {
|
|
266
|
+
console.error(
|
|
267
|
+
`[withdraw] owner balances before: ${formatToken(owner0Before, token0Decimals, token0Symbol)}, ${formatToken(owner1Before, token1Decimals, token1Symbol)}`
|
|
268
|
+
);
|
|
269
|
+
console.error(
|
|
270
|
+
`[withdraw] vault balances before: ${formatToken(vault0Before, token0Decimals, token0Symbol)}, ${formatToken(vault1Before, token1Decimals, token1Symbol)}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const data = encodeWithdrawData(shares, ownerAddress);
|
|
275
|
+
const chain = viemChain || (await resolveViemChainFromClient(client));
|
|
276
|
+
const hash = await sendTransaction(client, {
|
|
277
|
+
account: ownerAddress,
|
|
278
|
+
to: vaultAddress,
|
|
279
|
+
data,
|
|
280
|
+
chain,
|
|
281
|
+
});
|
|
282
|
+
if (debug) console.error(`[withdraw] tx sent: ${hash}`);
|
|
283
|
+
await client.mine({ blocks: 1 });
|
|
284
|
+
|
|
285
|
+
const [owner0After, owner1After, vault0After, vault1After] = await Promise.all([
|
|
286
|
+
client.readContract({
|
|
287
|
+
address: token0Address,
|
|
288
|
+
abi: ERC20_ABI,
|
|
289
|
+
functionName: 'balanceOf',
|
|
290
|
+
args: [ownerAddress],
|
|
291
|
+
}),
|
|
292
|
+
client.readContract({
|
|
293
|
+
address: token1Address,
|
|
294
|
+
abi: ERC20_ABI,
|
|
295
|
+
functionName: 'balanceOf',
|
|
296
|
+
args: [ownerAddress],
|
|
297
|
+
}),
|
|
298
|
+
client.readContract({
|
|
299
|
+
address: token0Address,
|
|
300
|
+
abi: ERC20_ABI,
|
|
301
|
+
functionName: 'balanceOf',
|
|
302
|
+
args: [vaultAddress],
|
|
303
|
+
}),
|
|
304
|
+
client.readContract({
|
|
305
|
+
address: token1Address,
|
|
306
|
+
abi: ERC20_ABI,
|
|
307
|
+
functionName: 'balanceOf',
|
|
308
|
+
args: [vaultAddress],
|
|
309
|
+
}),
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
if (debug) {
|
|
313
|
+
console.error(
|
|
314
|
+
`[withdraw] owner balances after : ${formatToken(owner0After, token0Decimals, token0Symbol)}, ${formatToken(owner1After, token1Decimals, token1Symbol)}`
|
|
315
|
+
);
|
|
316
|
+
console.error(
|
|
317
|
+
`[withdraw] vault balances after : ${formatToken(vault0After, token0Decimals, token0Symbol)}, ${formatToken(vault1After, token1Decimals, token1Symbol)}`
|
|
318
|
+
);
|
|
319
|
+
console.error(
|
|
320
|
+
`[withdraw] owner deltas: ${formatToken(BigInt(owner0After) - BigInt(owner0Before), token0Decimals, token0Symbol)}, ${formatToken(BigInt(owner1After) - BigInt(owner1Before), token1Decimals, token1Symbol)}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function encodeWithdrawData(shares, ownerAddress) {
|
|
328
|
+
return encodeFunctionData({
|
|
329
|
+
abi: VAULT_ABI,
|
|
330
|
+
functionName: 'withdraw',
|
|
331
|
+
args: [shares, 0n, 0n, ownerAddress],
|
|
332
|
+
});
|
|
333
|
+
}
|