@steerprotocol/liquidity-meter 1.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 +207 -0
- package/dist/adapters/onchain.d.ts +51 -0
- package/dist/adapters/onchain.js +158 -0
- package/dist/adapters/uniswapv3-subgraph.d.ts +44 -0
- package/dist/adapters/uniswapv3-subgraph.js +105 -0
- package/dist/adapters/withdraw.d.ts +14 -0
- package/dist/adapters/withdraw.js +150 -0
- package/dist/api.d.ts +72 -0
- package/dist/api.js +180 -0
- package/dist/cli/liquidity-depth.d.ts +2 -0
- package/dist/cli/liquidity-depth.js +1160 -0
- package/dist/core/depth.d.ts +48 -0
- package/dist/core/depth.js +314 -0
- package/dist/handlers/cron.d.ts +27 -0
- package/dist/handlers/cron.js +68 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/prices.d.ts +15 -0
- package/dist/prices.js +205 -0
- package/dist/wagmi/config.d.ts +2106 -0
- package/dist/wagmi/config.js +24 -0
- package/dist/wagmi/generated.d.ts +2019 -0
- package/dist/wagmi/generated.js +346 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI for computing Uniswap v3 market depth bands with real data (TypeScript)
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execFile as _execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const execFile = promisify(_execFile);
|
|
8
|
+
import { createMemoryClient, http } from 'tevm';
|
|
9
|
+
import { getAddress, encodeFunctionData, parseEther, createPublicClient, http as viemHttp } from 'viem';
|
|
10
|
+
import { sendTransaction as viemSendTransaction } from 'viem/actions';
|
|
11
|
+
import * as tevmCommon from 'tevm/common';
|
|
12
|
+
import * as viemChains from 'viem/chains';
|
|
13
|
+
import { computeMarketDepthBands, computePriceImpactsBySizes } from '../core/depth.js';
|
|
14
|
+
import { fetchPoolSnapshot as fetchFromSubgraph } from '../adapters/uniswapv3-subgraph.js';
|
|
15
|
+
import { fetchPoolSnapshotViem } from '../adapters/onchain.js';
|
|
16
|
+
import { fetchTokenUSDPrices } from '../prices.js';
|
|
17
|
+
import { erc20Abi, steerVaultAbi, uniswapV3FactoryAbi } from '../wagmi/generated.js';
|
|
18
|
+
const UNISWAP_V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984';
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = {
|
|
21
|
+
percent: '1,2,5,10',
|
|
22
|
+
subgraph: undefined,
|
|
23
|
+
source: 'tevm',
|
|
24
|
+
rpc: process.env.RPC_URL,
|
|
25
|
+
usdSizes: '1000,5000,10000,25000,50000,100000',
|
|
26
|
+
prices: process.env.PRICES_SOURCE || 'auto',
|
|
27
|
+
reserveLimit: Number(process.env.RESERVE_LIMIT) || 100,
|
|
28
|
+
csv: undefined,
|
|
29
|
+
outdir: 'reports',
|
|
30
|
+
// withdraw simulation
|
|
31
|
+
vault: undefined,
|
|
32
|
+
owner: process.env.OWNER,
|
|
33
|
+
withdrawPct: undefined,
|
|
34
|
+
withdrawShares: undefined,
|
|
35
|
+
};
|
|
36
|
+
for (let i = 2; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (a === '--pool' || a === '-p')
|
|
39
|
+
args.pool = argv[++i];
|
|
40
|
+
else if (a === '--percent' || a === '-b')
|
|
41
|
+
args.percent = argv[++i];
|
|
42
|
+
else if (a === '--subgraph' || a === '-s')
|
|
43
|
+
args.subgraph = argv[++i];
|
|
44
|
+
else if (a === '--rpc' || a === '-r')
|
|
45
|
+
args.rpc = argv[++i];
|
|
46
|
+
else if (a === '--source')
|
|
47
|
+
args.source = argv[++i]; // tevm|viem|subgraph|auto
|
|
48
|
+
else if (a === '--token0-usd')
|
|
49
|
+
args.token0USD = Number(argv[++i]);
|
|
50
|
+
else if (a === '--token1-usd')
|
|
51
|
+
args.token1USD = Number(argv[++i]);
|
|
52
|
+
else if (a === '--assume-stable')
|
|
53
|
+
args.assumeStable = Number(argv[++i]); // 0 or 1
|
|
54
|
+
else if (a === '--usd-sizes')
|
|
55
|
+
args.usdSizes = argv[++i];
|
|
56
|
+
else if (a === '--prices')
|
|
57
|
+
args.prices = argv[++i]; // llama|coingecko|auto
|
|
58
|
+
// withdraw simulation flags
|
|
59
|
+
else if (a === '--vault')
|
|
60
|
+
args.vault = argv[++i];
|
|
61
|
+
else if (a === '--owner')
|
|
62
|
+
args.owner = argv[++i];
|
|
63
|
+
else if (a === '--withdraw-pct' || a === '--withdraw-percent')
|
|
64
|
+
args.withdrawPct = Number(argv[++i]);
|
|
65
|
+
else if (a === '--withdraw-shares')
|
|
66
|
+
args.withdrawShares = argv[++i];
|
|
67
|
+
else if (a === '--debug')
|
|
68
|
+
args.debug = true;
|
|
69
|
+
else if (a === '--csv')
|
|
70
|
+
args.csv = argv[++i];
|
|
71
|
+
else if (a === '--outdir')
|
|
72
|
+
args.outdir = argv[++i];
|
|
73
|
+
else if (a === '--reserve-limit')
|
|
74
|
+
args.reserveLimit = Number(argv[++i]);
|
|
75
|
+
else if (a === '--throttle-ms')
|
|
76
|
+
args.throttleMs = Number(argv[++i]);
|
|
77
|
+
else if (a === '--retries')
|
|
78
|
+
args.retries = Number(argv[++i]);
|
|
79
|
+
else if (a === '--json' || a === '-j')
|
|
80
|
+
args.json = true;
|
|
81
|
+
else if (a === '--help' || a === '-h')
|
|
82
|
+
args.help = true;
|
|
83
|
+
else if (a === '--version' || a === '-v')
|
|
84
|
+
args.version = true;
|
|
85
|
+
else if (!args.pool && looksLikeAddress(a))
|
|
86
|
+
args.pool = a;
|
|
87
|
+
else {
|
|
88
|
+
console.error(`Unknown arg: ${a}`);
|
|
89
|
+
args.help = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return args;
|
|
93
|
+
}
|
|
94
|
+
function printHelp() {
|
|
95
|
+
console.log(`
|
|
96
|
+
liquidity-depth — Uniswap v3 market depth bands
|
|
97
|
+
|
|
98
|
+
Usage:
|
|
99
|
+
liquidity-depth --pool <POOL_ADDRESS> [--percent 1,2,5,10] [--source tevm|viem|subgraph|auto] [--rpc <URL>] [--token0-usd N] [--token1-usd N] [--usd-sizes 1000,5000,...] [--json]
|
|
100
|
+
liquidity-depth --pool <POOL_ADDRESS> --vault <VAULT_ADDRESS> --owner <OWNER_ADDRESS> [--withdraw-pct N | --withdraw-shares <SHARES>] [same options]
|
|
101
|
+
liquidity-depth --csv <FILE.csv> [--outdir reports] [same options as above]
|
|
102
|
+
|
|
103
|
+
Options:
|
|
104
|
+
-p, --pool Uniswap v3 pool address (e.g., 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8)
|
|
105
|
+
-b, --percent Percent buckets, comma-separated (default: 1,2,5,10)
|
|
106
|
+
-s, --subgraph Subgraph URL (default: Uniswap v3 mainnet)
|
|
107
|
+
--source Data source: tevm|viem|subgraph|auto (default: tevm)
|
|
108
|
+
-r, --rpc RPC URL (for on-chain fetching via tevm fork). Also reads RPC_URL env var
|
|
109
|
+
--token0-usd Override token0 USD price
|
|
110
|
+
--token1-usd Override token1 USD price
|
|
111
|
+
--assume-stable 0|1 Assume token index is $1 stable (fills missing USD)
|
|
112
|
+
--usd-sizes Comma-separated USD sizes for price impact (default: 1000,5000,10000,25000,50000,100000)
|
|
113
|
+
--prices Price source: reserve|dexscreener|llama|coingecko|auto (default: auto; env PRICES_SOURCE)
|
|
114
|
+
--reserve-limit N Limit for Reserve API (default: 100; env RESERVE_LIMIT)
|
|
115
|
+
--throttle-ms N Delay between CSV rows & retries (ms)
|
|
116
|
+
--retries N Retries per row on transient RPC errors (default: 0)
|
|
117
|
+
--debug Print debug logs to stderr for pricing/inference
|
|
118
|
+
--csv CSV file with a header including a 'pool' or 'address' column
|
|
119
|
+
--outdir Output directory for per-pool reports (default: reports)
|
|
120
|
+
--vault Steer vault address (to simulate withdrawal)
|
|
121
|
+
--owner Owner address whose shares are withdrawn (also reads OWNER env)
|
|
122
|
+
--withdraw-pct Percent of owner's shares to withdraw (e.g., 25)
|
|
123
|
+
--withdraw-shares Raw share amount to withdraw (overrides --withdraw-pct)
|
|
124
|
+
-j, --json Output machine-readable JSON
|
|
125
|
+
-h, --help Show help
|
|
126
|
+
-v, --version Show version
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
function looksLikeAddress(x) {
|
|
130
|
+
return typeof x === 'string' && /^0x[0-9a-fA-F]{40}$/.test(x.trim());
|
|
131
|
+
}
|
|
132
|
+
function extractFirstAddress(x) {
|
|
133
|
+
if (typeof x !== 'string')
|
|
134
|
+
return undefined;
|
|
135
|
+
const m = x.match(/0x[0-9a-fA-F]{40}/);
|
|
136
|
+
return m ? m[0] : undefined;
|
|
137
|
+
}
|
|
138
|
+
function normKey(k) {
|
|
139
|
+
return String(k || '').toLowerCase().replace(/[ _-]+/g, '');
|
|
140
|
+
}
|
|
141
|
+
// Minimal CSV parser: returns array of row objects keyed by header.
|
|
142
|
+
// Supports commas, quoted fields with double-quotes escaping.
|
|
143
|
+
function parseCSV(content) {
|
|
144
|
+
const outRows = [];
|
|
145
|
+
let i = 0;
|
|
146
|
+
const len = content.length;
|
|
147
|
+
const fields = [];
|
|
148
|
+
function readLine() {
|
|
149
|
+
fields.length = 0;
|
|
150
|
+
let field = '';
|
|
151
|
+
let inQuotes = false;
|
|
152
|
+
let sawAny = false;
|
|
153
|
+
while (i < len) {
|
|
154
|
+
const ch = content[i++];
|
|
155
|
+
if (!inQuotes && (ch === '\n' || ch === '\r')) {
|
|
156
|
+
if (ch === '\r' && content[i] === '\n')
|
|
157
|
+
i++; // CRLF
|
|
158
|
+
fields.push(field);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (ch === '"') {
|
|
162
|
+
if (inQuotes) {
|
|
163
|
+
if (content[i] === '"') {
|
|
164
|
+
field += '"';
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
inQuotes = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
inQuotes = true;
|
|
173
|
+
sawAny = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (ch === ',' && !inQuotes) {
|
|
177
|
+
fields.push(field);
|
|
178
|
+
field = '';
|
|
179
|
+
sawAny = true;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
field += ch;
|
|
183
|
+
sawAny = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!sawAny && field === '')
|
|
187
|
+
return false; // EOF after last newline
|
|
188
|
+
fields.push(field);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
// read header
|
|
192
|
+
if (!readLine())
|
|
193
|
+
return [];
|
|
194
|
+
const header = fields.map((h) => h.trim());
|
|
195
|
+
while (readLine()) {
|
|
196
|
+
const obj = {};
|
|
197
|
+
for (let j = 0; j < header.length; j++)
|
|
198
|
+
obj[header[j]] = (fields[j] ?? '').trim();
|
|
199
|
+
outRows.push(obj);
|
|
200
|
+
}
|
|
201
|
+
return outRows;
|
|
202
|
+
}
|
|
203
|
+
async function runBatch(args) {
|
|
204
|
+
const csvPath = args.csv;
|
|
205
|
+
const outdir = args.outdir || 'reports';
|
|
206
|
+
const data = await fs.readFile(csvPath, 'utf8');
|
|
207
|
+
const rows = parseCSV(data);
|
|
208
|
+
if (!rows.length) {
|
|
209
|
+
console.error('CSV is empty or could not be parsed. Expect a header row with a pool/address column.');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
213
|
+
const throttle = async (ms) => {
|
|
214
|
+
if (!ms || ms <= 0)
|
|
215
|
+
return;
|
|
216
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
217
|
+
};
|
|
218
|
+
const pick = (obj, names) => {
|
|
219
|
+
// try direct keys
|
|
220
|
+
for (const n of names)
|
|
221
|
+
if (obj[n] != null && String(obj[n]).trim() !== '')
|
|
222
|
+
return String(obj[n]).trim();
|
|
223
|
+
// try normalized keys
|
|
224
|
+
const map = Object.fromEntries(Object.keys(obj).map((k) => [normKey(k), k]));
|
|
225
|
+
for (const n of names) {
|
|
226
|
+
const key = map[normKey(n)];
|
|
227
|
+
if (key && obj[key] != null && String(obj[key]).trim() !== '')
|
|
228
|
+
return String(obj[key]).trim();
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
};
|
|
232
|
+
function parseFeeToUint24(v) {
|
|
233
|
+
if (v == null)
|
|
234
|
+
return undefined;
|
|
235
|
+
const s = String(v).trim();
|
|
236
|
+
if (!s)
|
|
237
|
+
return undefined;
|
|
238
|
+
// raw number like 500, 3000, 10000
|
|
239
|
+
if (/^\d+$/.test(s)) {
|
|
240
|
+
const n = Number(s);
|
|
241
|
+
if (Number.isFinite(n) && n > 0)
|
|
242
|
+
return n;
|
|
243
|
+
}
|
|
244
|
+
// like 30bps or 5 bps
|
|
245
|
+
const mBps = s.match(/([0-9]+(?:\.[0-9]+)?)\s*bps/i);
|
|
246
|
+
if (mBps) {
|
|
247
|
+
const n = Number(mBps[1]);
|
|
248
|
+
if (Number.isFinite(n))
|
|
249
|
+
return Math.round(n * 100);
|
|
250
|
+
}
|
|
251
|
+
// like 0.3% or 1 %
|
|
252
|
+
const mPct = s.match(/([0-9]+(?:\.[0-9]+)?)\s*%/);
|
|
253
|
+
if (mPct) {
|
|
254
|
+
const n = Number(mPct[1]);
|
|
255
|
+
if (Number.isFinite(n))
|
|
256
|
+
return Math.round(n * 1e4);
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
async function resolvePoolFromTokens(row, rpcUrl) {
|
|
261
|
+
// attempt to find token addresses + fee
|
|
262
|
+
let t0 = pick(row, ['token0', 'token0_address', 'token 0', 'token 0 address', 'tokenA', 'base']);
|
|
263
|
+
let t1 = pick(row, ['token1', 'token1_address', 'token 1', 'token 1 address', 'tokenB', 'quote']);
|
|
264
|
+
let fee = pick(row, ['fee', 'fee_tier', 'fee tier', 'feebps']);
|
|
265
|
+
// If not found, scan all values for first two distinct addresses
|
|
266
|
+
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '')) {
|
|
267
|
+
const found = [];
|
|
268
|
+
for (const v of Object.values(row)) {
|
|
269
|
+
const addr = extractFirstAddress(String(v || ''));
|
|
270
|
+
if (addr && !found.includes(addr))
|
|
271
|
+
found.push(addr);
|
|
272
|
+
if (found.length >= 2)
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (!looksLikeAddress(t0 || '') && found[0])
|
|
276
|
+
t0 = found[0];
|
|
277
|
+
if (!looksLikeAddress(t1 || '') && found[1])
|
|
278
|
+
t1 = found[1];
|
|
279
|
+
}
|
|
280
|
+
// parse fee
|
|
281
|
+
const feeNum = parseFeeToUint24(fee);
|
|
282
|
+
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '') || !feeNum)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (!rpcUrl)
|
|
285
|
+
return undefined;
|
|
286
|
+
const client = createMemoryClient({ fork: { transport: http(rpcUrl), blockTag: 'latest' }, loggingLevel: 'error' });
|
|
287
|
+
if (client.tevmReady)
|
|
288
|
+
await client.tevmReady();
|
|
289
|
+
try {
|
|
290
|
+
const pool = await client.readContract({ address: UNISWAP_V3_FACTORY, abi: uniswapV3FactoryAbi, functionName: 'getPool', args: [getAddress(t0), getAddress(t1), feeNum] });
|
|
291
|
+
if (pool && pool !== '0x0000000000000000000000000000000000000000')
|
|
292
|
+
return pool;
|
|
293
|
+
}
|
|
294
|
+
catch (_) {
|
|
295
|
+
// ignore
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
// base args to pass through from CLI
|
|
300
|
+
const base = [];
|
|
301
|
+
if (args.percent)
|
|
302
|
+
base.push('--percent', String(args.percent));
|
|
303
|
+
if (args.source)
|
|
304
|
+
base.push('--source', String(args.source));
|
|
305
|
+
if (args.rpc)
|
|
306
|
+
base.push('--rpc', String(args.rpc));
|
|
307
|
+
if (args.subgraph)
|
|
308
|
+
base.push('--subgraph', String(args.subgraph));
|
|
309
|
+
if (Number.isFinite(args.token0USD))
|
|
310
|
+
base.push('--token0-usd', String(args.token0USD));
|
|
311
|
+
if (Number.isFinite(args.token1USD))
|
|
312
|
+
base.push('--token1-usd', String(args.token1USD));
|
|
313
|
+
if (Number.isFinite(args.assumeStable))
|
|
314
|
+
base.push('--assume-stable', String(args.assumeStable));
|
|
315
|
+
if (args.usdSizes)
|
|
316
|
+
base.push('--usd-sizes', String(args.usdSizes));
|
|
317
|
+
if (args.prices)
|
|
318
|
+
base.push('--prices', String(args.prices));
|
|
319
|
+
if (args.reserveLimit)
|
|
320
|
+
base.push('--reserve-limit', String(args.reserveLimit));
|
|
321
|
+
if (args.json)
|
|
322
|
+
base.push('--json');
|
|
323
|
+
if (args.debug)
|
|
324
|
+
base.push('--debug');
|
|
325
|
+
if (args.owner)
|
|
326
|
+
base.push('--owner', String(args.owner));
|
|
327
|
+
if (Number.isFinite(args.withdrawPct))
|
|
328
|
+
base.push('--withdraw-pct', String(args.withdrawPct));
|
|
329
|
+
if (args.withdrawShares != null)
|
|
330
|
+
base.push('--withdraw-shares', String(args.withdrawShares));
|
|
331
|
+
const scriptPath = process.argv[1];
|
|
332
|
+
for (let idx = 0; idx < rows.length; idx++) {
|
|
333
|
+
const row = rows[idx];
|
|
334
|
+
let pool = pick(row, ['pool', 'address', 'pool_address', 'id', 'Pool', 'Address', 'contract_address', 'contract address', 'pair_address', 'pair address']);
|
|
335
|
+
if (!looksLikeAddress(pool || '')) {
|
|
336
|
+
// try extract from any cell (e.g., Etherscan/Uniswap URL)
|
|
337
|
+
for (const v of Object.values(row)) {
|
|
338
|
+
const found = extractFirstAddress(String(v || ''));
|
|
339
|
+
if (found) {
|
|
340
|
+
pool = found;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!looksLikeAddress(pool || '')) {
|
|
346
|
+
// try factory lookup using tokens + fee
|
|
347
|
+
const viaFactory = await resolvePoolFromTokens(row, args.rpc);
|
|
348
|
+
if (viaFactory)
|
|
349
|
+
pool = viaFactory;
|
|
350
|
+
}
|
|
351
|
+
if (!looksLikeAddress(pool || '')) {
|
|
352
|
+
console.warn(`Row ${idx + 1}: skipping (no valid pool address)`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const per = [...base];
|
|
356
|
+
const rpcs = pick(row, ['rpc', 'RPC', 'rpc_url', 'RPC_URL']);
|
|
357
|
+
if (rpcs)
|
|
358
|
+
per.push('--rpc', rpcs);
|
|
359
|
+
const source = pick(row, ['source', 'Source']);
|
|
360
|
+
if (source)
|
|
361
|
+
per.push('--source', source);
|
|
362
|
+
const percent = pick(row, ['percent', 'Percents', 'buckets']);
|
|
363
|
+
if (percent)
|
|
364
|
+
per.push('--percent', percent);
|
|
365
|
+
const token0USD = pick(row, ['token0_usd', 'token0-usd', 'token0USD']);
|
|
366
|
+
const token1USD = pick(row, ['token1_usd', 'token1-usd', 'token1USD']);
|
|
367
|
+
if (token0USD)
|
|
368
|
+
per.push('--token0-usd', String(token0USD));
|
|
369
|
+
if (token1USD)
|
|
370
|
+
per.push('--token1-usd', String(token1USD));
|
|
371
|
+
const usdSizes = pick(row, ['usd_sizes', 'usd-sizes', 'usdSizes']);
|
|
372
|
+
if (usdSizes)
|
|
373
|
+
per.push('--usd-sizes', usdSizes);
|
|
374
|
+
const assumeStable = pick(row, ['assume_stable', 'assume-stable', 'assumeStable']);
|
|
375
|
+
if (assumeStable != null)
|
|
376
|
+
per.push('--assume-stable', String(assumeStable));
|
|
377
|
+
const prices = pick(row, ['prices', 'Prices']);
|
|
378
|
+
if (prices)
|
|
379
|
+
per.push('--prices', String(prices));
|
|
380
|
+
const vault = pick(row, ['vault', 'Vault', 'vault_address', 'VaultAddress', 'vault address', 'strategy']);
|
|
381
|
+
if (vault)
|
|
382
|
+
per.push('--vault', vault);
|
|
383
|
+
const owner = pick(row, ['owner', 'Owner', 'owner_address', 'owner address', 'withdraw_owner', 'withdraw owner', 'account', 'user', 'recipient', 'to']);
|
|
384
|
+
if (owner)
|
|
385
|
+
per.push('--owner', owner);
|
|
386
|
+
const wpct = pick(row, ['withdraw_pct', 'withdraw-pct', 'withdraw percent', 'withdraw_percent', 'withdrawPercentage', 'withdrawPercent', 'pct_withdraw', 'percent_withdraw']);
|
|
387
|
+
if (wpct)
|
|
388
|
+
per.push('--withdraw-pct', String(wpct));
|
|
389
|
+
const wshares = pick(row, ['withdraw_shares', 'withdraw-shares', 'withdraw shares', 'withdrawShares', 'shares_to_withdraw', 'withdraw_share_amount']);
|
|
390
|
+
if (wshares)
|
|
391
|
+
per.push('--withdraw-shares', String(wshares));
|
|
392
|
+
if (args.debug) {
|
|
393
|
+
const dbg = [];
|
|
394
|
+
if (vault)
|
|
395
|
+
dbg.push(`vault=${vault}`);
|
|
396
|
+
if (owner)
|
|
397
|
+
dbg.push(`owner=${owner}`);
|
|
398
|
+
if (wpct)
|
|
399
|
+
dbg.push(`withdraw_pct=${wpct}`);
|
|
400
|
+
if (wshares)
|
|
401
|
+
dbg.push(`withdraw_shares=${wshares}`);
|
|
402
|
+
if (dbg.length)
|
|
403
|
+
console.error(`[batch] row ${idx + 1} overrides: ${dbg.join(' ')}`);
|
|
404
|
+
}
|
|
405
|
+
const poolStr = pool;
|
|
406
|
+
const argsArr = [scriptPath, '--pool', poolStr, ...per];
|
|
407
|
+
const maxRetries = Number.isFinite(args.retries) ? Math.max(0, Number(args.retries)) : 0;
|
|
408
|
+
let attempt = 0;
|
|
409
|
+
let done = false;
|
|
410
|
+
let lastErr = null;
|
|
411
|
+
while (!done && attempt <= maxRetries) {
|
|
412
|
+
try {
|
|
413
|
+
const { stdout, stderr } = await execFile(process.execPath, argsArr, { env: process.env, maxBuffer: 5 * 1024 * 1024 });
|
|
414
|
+
const baseName = `${poolStr.toLowerCase()}`;
|
|
415
|
+
const file = path.join(outdir, `${baseName}.${args.json ? 'json' : 'txt'}`);
|
|
416
|
+
const cleaned = cleanReportOutput(stdout);
|
|
417
|
+
await fs.writeFile(file, cleaned, 'utf8');
|
|
418
|
+
if (args.debug && stderr)
|
|
419
|
+
process.stderr.write(stderr);
|
|
420
|
+
console.log(`Wrote: ${file}`);
|
|
421
|
+
done = true;
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
lastErr = err;
|
|
425
|
+
const errText = String(err?.stderr || err?.stdout || err?.message || err || '');
|
|
426
|
+
const isTransient = /jsonrpc|Unknown error in jsonrpc request|proof window|ETIMEOUT|ECONNRESET|429|rate limit/i.test(errText);
|
|
427
|
+
const willRetry = attempt < maxRetries && isTransient;
|
|
428
|
+
if (args.debug)
|
|
429
|
+
console.error(`[batch] row ${idx + 1} attempt ${attempt + 1}/${maxRetries + 1} ${willRetry ? 'retrying' : 'failed'}: ${errText.split('\n')[0]}`);
|
|
430
|
+
if (!willRetry)
|
|
431
|
+
break;
|
|
432
|
+
const backoff = (args.throttleMs ?? 0) + Math.min(2000, 250 * (attempt + 1));
|
|
433
|
+
await throttle(backoff);
|
|
434
|
+
attempt += 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (!done) {
|
|
438
|
+
const errMsg = String(lastErr?.stderr || lastErr?.stdout || lastErr?.message || lastErr || '');
|
|
439
|
+
const errFile = path.join(outdir, `error-${poolStr.toLowerCase()}.txt`);
|
|
440
|
+
await fs.writeFile(errFile, errMsg, 'utf8');
|
|
441
|
+
console.error(`Failed row ${idx + 1} (${poolStr}): ${lastErr?.message || lastErr}`);
|
|
442
|
+
console.error(` -> see ${errFile}`);
|
|
443
|
+
}
|
|
444
|
+
// throttle between rows to respect provider limits
|
|
445
|
+
await throttle(args.throttleMs);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Strip noisy logs (e.g., TEVM warnings) from report files while preserving CLI output
|
|
449
|
+
function cleanReportOutput(s) {
|
|
450
|
+
try {
|
|
451
|
+
const lines = String(s || '').split(/\r?\n/);
|
|
452
|
+
const out = [];
|
|
453
|
+
for (const line of lines) {
|
|
454
|
+
if (!line) {
|
|
455
|
+
out.push(line);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// Filter known noisy patterns
|
|
459
|
+
if (line.includes('"name":"@tevm/'))
|
|
460
|
+
continue; // TEVM structured logs
|
|
461
|
+
if (/\bEIP-7702\b/i.test(line))
|
|
462
|
+
continue; // EIP-7702 warning block
|
|
463
|
+
if (/^\s*\{\s*chainId\s*:/i.test(line))
|
|
464
|
+
continue; // stray chainId object
|
|
465
|
+
if (/^\s*0x[0-9a-fA-F]+\s*$/.test(line))
|
|
466
|
+
continue; // bare hex line
|
|
467
|
+
out.push(line);
|
|
468
|
+
}
|
|
469
|
+
// Trim leading empty lines left by filtering
|
|
470
|
+
while (out.length && out[0].trim() === '')
|
|
471
|
+
out.shift();
|
|
472
|
+
return out.join('\n');
|
|
473
|
+
}
|
|
474
|
+
catch (_) {
|
|
475
|
+
return s;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function main() {
|
|
479
|
+
const args = parseArgs(process.argv);
|
|
480
|
+
if (args.version) {
|
|
481
|
+
console.log('liquidity-depth 0.1.0');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (args.help || (!args.pool && !args.csv)) {
|
|
485
|
+
printHelp();
|
|
486
|
+
if (!args.pool && !args.csv)
|
|
487
|
+
process.exitCode = 1;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (args.csv) {
|
|
491
|
+
await runBatch(args);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const percentBuckets = args.percent
|
|
495
|
+
.split(',')
|
|
496
|
+
.map((x) => Number(x.trim()))
|
|
497
|
+
.filter((x) => Number.isFinite(x) && x > 0)
|
|
498
|
+
.sort((a, b) => a - b);
|
|
499
|
+
if (!percentBuckets.length) {
|
|
500
|
+
console.error('No valid percent buckets parsed.');
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
// Choose data source and initialize clients as needed (match original JS behavior)
|
|
504
|
+
let client = null;
|
|
505
|
+
let usedSource = args.source;
|
|
506
|
+
if (args.source === 'viem' || args.source === 'tevm' || (args.source === 'auto' && args.rpc)) {
|
|
507
|
+
if (!args.rpc) {
|
|
508
|
+
console.error('Missing --rpc (or RPC_URL env) for tevm source.');
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
const common = await resolveTevmCommon(args.rpc);
|
|
512
|
+
client = createMemoryClient({ fork: { transport: http(args.rpc), blockTag: 'latest' }, loggingLevel: 'error', ...(common ? { common } : {}) });
|
|
513
|
+
if (client.tevmReady)
|
|
514
|
+
await client.tevmReady();
|
|
515
|
+
usedSource = 'tevm';
|
|
516
|
+
}
|
|
517
|
+
else if (args.source === 'subgraph' || args.source === 'auto') {
|
|
518
|
+
usedSource = 'subgraph';
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
console.error(`Unknown --source: ${args.source}`);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
// Fetch pool snapshot + ticks in range (via selected source)
|
|
525
|
+
let snap;
|
|
526
|
+
if (usedSource === 'tevm') {
|
|
527
|
+
snap = await fetchPoolSnapshotViem({ poolAddress: args.pool, rpcUrl: args.rpc, percentBuckets, client: client || undefined });
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
snap = await fetchFromSubgraph({ poolAddress: args.pool, percentBuckets, subgraphUrl: args.subgraph });
|
|
531
|
+
}
|
|
532
|
+
// Fill USD prices if provided or can infer from stable coin
|
|
533
|
+
const usd0 = Number.isFinite(args.token0USD) ? args.token0USD : snap.token0.usdPrice;
|
|
534
|
+
const usd1 = Number.isFinite(args.token1USD) ? args.token1USD : snap.token1.usdPrice;
|
|
535
|
+
// Infer/fetch missing USD using pool price, price APIs, or stable-coin hint
|
|
536
|
+
let token0USD = usd0;
|
|
537
|
+
let token1USD = usd1;
|
|
538
|
+
const dec0 = snap.token0.decimals;
|
|
539
|
+
const dec1 = snap.token1.decimals;
|
|
540
|
+
const price1Per0 = Math.pow(1.0001, snap.tick) * Math.pow(10, dec0 - dec1); // token1 tokens per 1 token0
|
|
541
|
+
if (!usd0 && usd1)
|
|
542
|
+
token0USD = usd1 * price1Per0;
|
|
543
|
+
if (usd0 && !usd1)
|
|
544
|
+
token1USD = usd0 / price1Per0;
|
|
545
|
+
let priceSource0 = usd0 ? 'override' : undefined;
|
|
546
|
+
let priceSource1 = usd1 ? 'override' : undefined;
|
|
547
|
+
if ((!token0USD || !token1USD)) {
|
|
548
|
+
// Try external price APIs if addresses present
|
|
549
|
+
try {
|
|
550
|
+
const addrs = [(snap.meta?.token0?.id || snap.token0.id), (snap.meta?.token1?.id || snap.token1.id)].map((x) => x.toLowerCase());
|
|
551
|
+
let chainIdForPrices;
|
|
552
|
+
try {
|
|
553
|
+
if (args.rpc) {
|
|
554
|
+
const clientCID = createMemoryClient({ fork: { transport: http(args.rpc), blockTag: 'latest' }, loggingLevel: 'error' });
|
|
555
|
+
if (clientCID.tevmReady)
|
|
556
|
+
await clientCID.tevmReady();
|
|
557
|
+
chainIdForPrices = await clientCID.getChainId();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch (_) { }
|
|
561
|
+
if (args.debug) {
|
|
562
|
+
console.error(`[cli] price fetch: addrs=${addrs.join(',')} source=${args.prices} rpc=${args.rpc ? 'yes' : 'no'} chainId=${chainIdForPrices ?? 'n/a'}`);
|
|
563
|
+
}
|
|
564
|
+
const { byAddress } = await fetchTokenUSDPrices({ addresses: addrs, source: args.prices, chainId: chainIdForPrices, rpcUrl: args.rpc, debug: !!args.debug, reserveLimit: args.reserveLimit });
|
|
565
|
+
const p0 = byAddress[addrs[0]];
|
|
566
|
+
const p1 = byAddress[addrs[1]];
|
|
567
|
+
if (!token0USD && p0) {
|
|
568
|
+
token0USD = p0.price;
|
|
569
|
+
priceSource0 = p0.source;
|
|
570
|
+
}
|
|
571
|
+
if (!token1USD && p1) {
|
|
572
|
+
token1USD = p1.price;
|
|
573
|
+
priceSource1 = p1.source;
|
|
574
|
+
}
|
|
575
|
+
// Cross-derive the missing side using pool price if only one side priced
|
|
576
|
+
if (token0USD && !token1USD) {
|
|
577
|
+
token1USD = token0USD / price1Per0;
|
|
578
|
+
if (!priceSource1)
|
|
579
|
+
priceSource1 = 'derived';
|
|
580
|
+
}
|
|
581
|
+
if (!token0USD && token1USD) {
|
|
582
|
+
token0USD = token1USD * price1Per0;
|
|
583
|
+
if (!priceSource0)
|
|
584
|
+
priceSource0 = 'derived';
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch (_) {
|
|
588
|
+
// ignore, fall back to heuristics below
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// If subgraph already provided USD
|
|
592
|
+
if (!priceSource0 && usd0)
|
|
593
|
+
priceSource0 = 'subgraph';
|
|
594
|
+
if (!priceSource1 && usd1)
|
|
595
|
+
priceSource1 = 'subgraph';
|
|
596
|
+
if ((!token0USD || !token1USD)) {
|
|
597
|
+
if (args.assumeStable === 0 || /USD|USDC|USDT|DAI/i.test((snap.meta?.token0?.symbol || ''))) {
|
|
598
|
+
if (!token0USD) {
|
|
599
|
+
token0USD = 1;
|
|
600
|
+
priceSource0 = 'inferred';
|
|
601
|
+
}
|
|
602
|
+
if (!token1USD) {
|
|
603
|
+
token1USD = 1 / price1Per0;
|
|
604
|
+
priceSource1 = 'inferred';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (args.assumeStable === 1 || /USD|USDC|USDT|DAI/i.test((snap.meta?.token1?.symbol || ''))) {
|
|
608
|
+
if (!token1USD) {
|
|
609
|
+
token1USD = 1;
|
|
610
|
+
priceSource1 = 'inferred';
|
|
611
|
+
}
|
|
612
|
+
if (!token0USD) {
|
|
613
|
+
token0USD = price1Per0;
|
|
614
|
+
priceSource0 = 'inferred';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (args.debug) {
|
|
619
|
+
console.error(`[cli] usd results: token0=${token0USD ?? 'n/a'} src0=${priceSource0 ?? 'n/a'} token1=${token1USD ?? 'n/a'} src1=${priceSource1 ?? 'n/a'}`);
|
|
620
|
+
}
|
|
621
|
+
// If still missing, mark source explicitly
|
|
622
|
+
if (!priceSource0 && token0USD)
|
|
623
|
+
priceSource0 = 'inferred';
|
|
624
|
+
if (!priceSource1 && token1USD)
|
|
625
|
+
priceSource1 = 'inferred';
|
|
626
|
+
const result = computeMarketDepthBands({
|
|
627
|
+
sqrtPriceX96: snap.sqrtPriceX96,
|
|
628
|
+
tick: snap.tick,
|
|
629
|
+
liquidity: snap.liquidity,
|
|
630
|
+
feePips: snap.feePips,
|
|
631
|
+
ticks: snap.ticks,
|
|
632
|
+
token0: { decimals: snap.token0.decimals, usdPrice: token0USD || 0 },
|
|
633
|
+
token1: { decimals: snap.token1.decimals, usdPrice: token1USD || 0 },
|
|
634
|
+
percentBuckets,
|
|
635
|
+
});
|
|
636
|
+
// Parse USD sizes for price impact
|
|
637
|
+
const usdSizes = args.usdSizes
|
|
638
|
+
.split(',')
|
|
639
|
+
.map((x) => Number(x.trim()))
|
|
640
|
+
.filter((x) => Number.isFinite(x) && x > 0)
|
|
641
|
+
.sort((a, b) => a - b);
|
|
642
|
+
// Convert USD -> raw token amounts (BigInt) with micro-dollar precision to avoid FP drift
|
|
643
|
+
function usdToRaw(usd, usdPrice, decimals) {
|
|
644
|
+
if (!usdPrice || usdPrice <= 0)
|
|
645
|
+
return 0n;
|
|
646
|
+
const micros = Math.round(usd * 1e6);
|
|
647
|
+
const usdMicrosPrice = Math.round(usdPrice * 1e6);
|
|
648
|
+
if (usdMicrosPrice <= 0)
|
|
649
|
+
return 0n;
|
|
650
|
+
const scale = 10n ** BigInt(decimals);
|
|
651
|
+
return (BigInt(micros) * scale) / BigInt(usdMicrosPrice);
|
|
652
|
+
}
|
|
653
|
+
let priceImpacts = null;
|
|
654
|
+
if (usdSizes.length && token0USD && token1USD) {
|
|
655
|
+
const buySizesToken1 = usdSizes.map((u) => usdToRaw(u, token1USD, snap.token1.decimals));
|
|
656
|
+
const sellSizesToken0 = usdSizes.map((u) => usdToRaw(u, token0USD, snap.token0.decimals));
|
|
657
|
+
priceImpacts = computePriceImpactsBySizes({
|
|
658
|
+
sqrtPriceX96: snap.sqrtPriceX96,
|
|
659
|
+
tick: snap.tick,
|
|
660
|
+
liquidity: snap.liquidity,
|
|
661
|
+
feePips: snap.feePips,
|
|
662
|
+
ticks: snap.ticks,
|
|
663
|
+
buySizesToken1,
|
|
664
|
+
sellSizesToken0,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// Helper to compute prices + metrics for a given snapshot (used for after-withdraw)
|
|
668
|
+
async function computeMetrics({ args, snap, usedSource, percentBuckets }) {
|
|
669
|
+
// Fill USD prices if provided or can infer from stable coin
|
|
670
|
+
const usd0 = Number.isFinite(args.token0USD) ? args.token0USD : snap.token0.usdPrice;
|
|
671
|
+
const usd1 = Number.isFinite(args.token1USD) ? args.token1USD : snap.token1.usdPrice;
|
|
672
|
+
let token0USD = usd0;
|
|
673
|
+
let token1USD = usd1;
|
|
674
|
+
const dec0 = snap.token0.decimals;
|
|
675
|
+
const dec1 = snap.token1.decimals;
|
|
676
|
+
const price1Per0 = Math.pow(1.0001, snap.tick) * Math.pow(10, dec0 - dec1);
|
|
677
|
+
if (!usd0 && usd1)
|
|
678
|
+
token0USD = usd1 * price1Per0;
|
|
679
|
+
if (usd0 && !usd1)
|
|
680
|
+
token1USD = usd0 / price1Per0;
|
|
681
|
+
let priceSource0 = usd0 ? 'override' : undefined;
|
|
682
|
+
let priceSource1 = usd1 ? 'override' : undefined;
|
|
683
|
+
if ((!token0USD || !token1USD)) {
|
|
684
|
+
try {
|
|
685
|
+
const addrs = [snap.meta?.token0?.id || snap.token0.id, snap.meta?.token1?.id || snap.token1.id].map((x) => x.toLowerCase());
|
|
686
|
+
let chainIdForPrices;
|
|
687
|
+
try {
|
|
688
|
+
if (args.rpc) {
|
|
689
|
+
const clientCID = createMemoryClient({ fork: { transport: http(args.rpc), blockTag: 'latest' }, loggingLevel: 'error' });
|
|
690
|
+
if (clientCID.tevmReady)
|
|
691
|
+
await clientCID.tevmReady();
|
|
692
|
+
chainIdForPrices = await clientCID.getChainId();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (_) { }
|
|
696
|
+
if (args.debug) {
|
|
697
|
+
console.error(`[cli] price fetch: addrs=${addrs.join(',')} source=${args.prices} rpc=${args.rpc ? 'yes' : 'no'} chainId=${chainIdForPrices ?? 'n/a'}`);
|
|
698
|
+
}
|
|
699
|
+
const { byAddress } = await fetchTokenUSDPrices({ addresses: addrs, source: args.prices, chainId: chainIdForPrices, rpcUrl: args.rpc, debug: !!args.debug, reserveLimit: args.reserveLimit });
|
|
700
|
+
const p0 = byAddress[addrs[0]];
|
|
701
|
+
const p1 = byAddress[addrs[1]];
|
|
702
|
+
if (!token0USD && p0) {
|
|
703
|
+
token0USD = p0.price;
|
|
704
|
+
priceSource0 = p0.source;
|
|
705
|
+
}
|
|
706
|
+
if (!token1USD && p1) {
|
|
707
|
+
token1USD = p1.price;
|
|
708
|
+
priceSource1 = p1.source;
|
|
709
|
+
}
|
|
710
|
+
if (token0USD && !token1USD) {
|
|
711
|
+
token1USD = token0USD / price1Per0;
|
|
712
|
+
if (!priceSource1)
|
|
713
|
+
priceSource1 = 'derived';
|
|
714
|
+
}
|
|
715
|
+
if (!token0USD && token1USD) {
|
|
716
|
+
token0USD = token1USD * price1Per0;
|
|
717
|
+
if (!priceSource0)
|
|
718
|
+
priceSource0 = 'derived';
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (_) { }
|
|
722
|
+
}
|
|
723
|
+
if (!priceSource0 && usd0)
|
|
724
|
+
priceSource0 = 'subgraph';
|
|
725
|
+
if (!priceSource1 && usd1)
|
|
726
|
+
priceSource1 = 'subgraph';
|
|
727
|
+
if ((!token0USD || !token1USD)) {
|
|
728
|
+
if (args.assumeStable === 0 || /USD|USDC|USDT|DAI/i.test(snap.meta?.token0?.symbol || '')) {
|
|
729
|
+
if (!token0USD) {
|
|
730
|
+
token0USD = 1;
|
|
731
|
+
priceSource0 = 'inferred';
|
|
732
|
+
}
|
|
733
|
+
if (!token1USD) {
|
|
734
|
+
token1USD = 1 / price1Per0;
|
|
735
|
+
priceSource1 = 'inferred';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else if (args.assumeStable === 1 || /USD|USDC|USDT|DAI/i.test(snap.meta?.token1?.symbol || '')) {
|
|
739
|
+
if (!token1USD) {
|
|
740
|
+
token1USD = 1;
|
|
741
|
+
priceSource1 = 'inferred';
|
|
742
|
+
}
|
|
743
|
+
if (!token0USD) {
|
|
744
|
+
token0USD = price1Per0;
|
|
745
|
+
priceSource0 = 'inferred';
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!priceSource0 && token0USD)
|
|
750
|
+
priceSource0 = 'inferred';
|
|
751
|
+
if (!priceSource1 && token1USD)
|
|
752
|
+
priceSource1 = 'inferred';
|
|
753
|
+
const result = computeMarketDepthBands({
|
|
754
|
+
sqrtPriceX96: snap.sqrtPriceX96,
|
|
755
|
+
tick: snap.tick,
|
|
756
|
+
liquidity: snap.liquidity,
|
|
757
|
+
feePips: snap.feePips,
|
|
758
|
+
ticks: snap.ticks,
|
|
759
|
+
token0: { decimals: snap.token0.decimals, usdPrice: token0USD || 0 },
|
|
760
|
+
token1: { decimals: snap.token1.decimals, usdPrice: token1USD || 0 },
|
|
761
|
+
percentBuckets,
|
|
762
|
+
});
|
|
763
|
+
const usdSizes = (args.usdSizes || '')
|
|
764
|
+
.split(',')
|
|
765
|
+
.map((x) => Number(x.trim()))
|
|
766
|
+
.filter((x) => Number.isFinite(x) && x > 0)
|
|
767
|
+
.sort((a, b) => a - b);
|
|
768
|
+
function usdToRaw(usd, usdPrice, decimals) {
|
|
769
|
+
if (!usdPrice || usdPrice <= 0)
|
|
770
|
+
return 0n;
|
|
771
|
+
const micros = Math.round(usd * 1e6);
|
|
772
|
+
const usdMicrosPrice = Math.round(usdPrice * 1e6);
|
|
773
|
+
if (usdMicrosPrice <= 0)
|
|
774
|
+
return 0n;
|
|
775
|
+
const scale = 10n ** BigInt(decimals);
|
|
776
|
+
return (BigInt(micros) * scale) / BigInt(usdMicrosPrice);
|
|
777
|
+
}
|
|
778
|
+
let priceImpacts = null;
|
|
779
|
+
if (usdSizes.length && token0USD && token1USD) {
|
|
780
|
+
const buySizesToken1 = usdSizes.map((u) => usdToRaw(u, token1USD, snap.token1.decimals));
|
|
781
|
+
const sellSizesToken0 = usdSizes.map((u) => usdToRaw(u, token0USD, snap.token0.decimals));
|
|
782
|
+
priceImpacts = computePriceImpactsBySizes({
|
|
783
|
+
sqrtPriceX96: snap.sqrtPriceX96,
|
|
784
|
+
tick: snap.tick,
|
|
785
|
+
liquidity: snap.liquidity,
|
|
786
|
+
feePips: snap.feePips,
|
|
787
|
+
ticks: snap.ticks,
|
|
788
|
+
buySizesToken1,
|
|
789
|
+
sellSizesToken0,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
return { token0USD, token1USD, priceSource0, priceSource1, priceImpacts, usdSizes, result, percentBuckets };
|
|
793
|
+
}
|
|
794
|
+
// If a vault withdrawal simulation is requested, perform it and compute the post-withdraw metrics using the same client/fork.
|
|
795
|
+
let after = null;
|
|
796
|
+
if (usedSource === 'tevm' && args.vault && (Number.isFinite(args.withdrawPct) || args.withdrawShares != null)) {
|
|
797
|
+
const viemChain = await resolveViemChain(args.rpc);
|
|
798
|
+
await simulateVaultWithdraw({ client: client, vault: args.vault, owner: args.owner, withdrawPct: args.withdrawPct, withdrawShares: args.withdrawShares, debug: !!args.debug, cliPool: args.pool, viemChain });
|
|
799
|
+
const snap2 = await fetchPoolSnapshotViem({ poolAddress: args.pool, client: client || undefined, percentBuckets });
|
|
800
|
+
if (args.debug) {
|
|
801
|
+
console.error(`[after] tick: ${snap.tick} -> ${snap2.tick}`);
|
|
802
|
+
console.error(`[after] liquidity: ${snap.liquidity.toString()} -> ${snap2.liquidity.toString()}`);
|
|
803
|
+
}
|
|
804
|
+
// Recompute prices and metrics for the post-withdraw state
|
|
805
|
+
const { token0USD: token0USD2, token1USD: token1USD2, priceSource0: priceSource0b, priceSource1: priceSource1b, priceImpacts: priceImpacts2, usdSizes: usdSizes2, result: result2, percentBuckets: percentBuckets2 } = await computeMetrics({ args, snap: snap2, usedSource, percentBuckets });
|
|
806
|
+
after = { snap: snap2, token0USD: token0USD2, token1USD: token1USD2, priceSource0: priceSource0b, priceSource1: priceSource1b, priceImpacts: priceImpacts2, usdSizes: usdSizes2, result: result2, percentBuckets: percentBuckets2 };
|
|
807
|
+
}
|
|
808
|
+
if (args.json && !after) {
|
|
809
|
+
console.log(JSON.stringify({
|
|
810
|
+
pool: snap.meta.poolId,
|
|
811
|
+
tokens: {
|
|
812
|
+
token0: snap.meta.token0,
|
|
813
|
+
token1: snap.meta.token1,
|
|
814
|
+
},
|
|
815
|
+
ethPriceUSD: snap.meta.ethPriceUSD,
|
|
816
|
+
tick: snap.tick,
|
|
817
|
+
sqrtPriceX96: String(snap.sqrtPriceX96),
|
|
818
|
+
liquidity: String(snap.liquidity),
|
|
819
|
+
feePips: snap.feePips,
|
|
820
|
+
range: snap.meta.range,
|
|
821
|
+
percentBuckets,
|
|
822
|
+
...result,
|
|
823
|
+
priceImpacts: priceImpacts
|
|
824
|
+
? { usdSizes, buyPct: priceImpacts.buyPct, sellPct: priceImpacts.sellPct }
|
|
825
|
+
: undefined,
|
|
826
|
+
prices: {
|
|
827
|
+
token0: { address: snap.meta.token0.id, symbol: snap.meta.token0.symbol, usd: token0USD, source: priceSource0 },
|
|
828
|
+
token1: { address: snap.meta.token1.id, symbol: snap.meta.token1.symbol, usd: token1USD, source: priceSource1 },
|
|
829
|
+
},
|
|
830
|
+
// convert BigInt arrays to string for JSON safety
|
|
831
|
+
buyCumToken1: result.buyCumToken1.map((x) => String(x)),
|
|
832
|
+
sellCumToken0: result.sellCumToken0.map((x) => String(x)),
|
|
833
|
+
}, null, 2));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (args.json && after) {
|
|
837
|
+
const out = {
|
|
838
|
+
before: {
|
|
839
|
+
pool: snap.meta.poolId,
|
|
840
|
+
tokens: { token0: snap.meta.token0, token1: snap.meta.token1 },
|
|
841
|
+
ethPriceUSD: snap.meta.ethPriceUSD,
|
|
842
|
+
tick: snap.tick,
|
|
843
|
+
sqrtPriceX96: String(snap.sqrtPriceX96),
|
|
844
|
+
liquidity: String(snap.liquidity),
|
|
845
|
+
feePips: snap.feePips,
|
|
846
|
+
range: snap.meta.range,
|
|
847
|
+
percentBuckets,
|
|
848
|
+
...result,
|
|
849
|
+
priceImpacts: priceImpacts ? { usdSizes, buyPct: priceImpacts.buyPct, sellPct: priceImpacts.sellPct } : undefined,
|
|
850
|
+
prices: {
|
|
851
|
+
token0: { address: snap.meta.token0.id, symbol: snap.meta.token0.symbol, usd: token0USD, source: priceSource0 },
|
|
852
|
+
token1: { address: snap.meta.token1.id, symbol: snap.meta.token1.symbol, usd: token1USD, source: priceSource1 },
|
|
853
|
+
},
|
|
854
|
+
buyCumToken1: result.buyCumToken1.map((x) => String(x)),
|
|
855
|
+
sellCumToken0: result.sellCumToken0.map((x) => String(x)),
|
|
856
|
+
},
|
|
857
|
+
after: {
|
|
858
|
+
pool: after.snap.meta.poolId,
|
|
859
|
+
tokens: { token0: after.snap.meta.token0, token1: after.snap.meta.token1 },
|
|
860
|
+
ethPriceUSD: after.snap.meta.ethPriceUSD,
|
|
861
|
+
tick: after.snap.tick,
|
|
862
|
+
sqrtPriceX96: String(after.snap.sqrtPriceX96),
|
|
863
|
+
liquidity: String(after.snap.liquidity),
|
|
864
|
+
feePips: after.snap.feePips,
|
|
865
|
+
range: after.snap.meta.range,
|
|
866
|
+
percentBuckets: after.percentBuckets,
|
|
867
|
+
...after.result,
|
|
868
|
+
priceImpacts: after.priceImpacts ? { usdSizes: after.usdSizes, buyPct: after.priceImpacts.buyPct, sellPct: after.priceImpacts.sellPct } : undefined,
|
|
869
|
+
prices: {
|
|
870
|
+
token0: { address: after.snap.meta.token0.id, symbol: after.snap.meta.token0.symbol, usd: after.token0USD, source: after.priceSource0 },
|
|
871
|
+
token1: { address: after.snap.meta.token1.id, symbol: after.snap.meta.token1.symbol, usd: after.token1USD, source: after.priceSource1 },
|
|
872
|
+
},
|
|
873
|
+
buyCumToken1: after.result.buyCumToken1.map((x) => String(x)),
|
|
874
|
+
sellCumToken0: after.result.sellCumToken0.map((x) => String(x)),
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
console.log(JSON.stringify(out, null, 2));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
// Pretty print summary
|
|
881
|
+
const fmtUSD = (n) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n);
|
|
882
|
+
console.log(`Pool: ${args.pool.toLowerCase()} [source: ${usedSource}]`);
|
|
883
|
+
console.log(`Tokens: ${snap.meta.token0.symbol} ${snap.meta.token0.id ? `(${snap.meta.token0.id})` : ''} / ${snap.meta.token1.symbol} ${snap.meta.token1.id ? `(${snap.meta.token1.id})` : ''}`);
|
|
884
|
+
if (snap.meta?.ethPriceUSD)
|
|
885
|
+
console.log(`ETH/USD: ${Number(snap.meta.ethPriceUSD).toFixed(2)}`);
|
|
886
|
+
const feePct = snap.feePips / 10_000;
|
|
887
|
+
const feeBps = snap.feePips / 100;
|
|
888
|
+
console.log(`Tick: ${snap.tick} Fee: ${feePct.toFixed(2)}% (${feeBps.toFixed(2)} bps)`);
|
|
889
|
+
if (token0USD || token1USD) {
|
|
890
|
+
const fmt = (v) => (v ? `$${v.toLocaleString(undefined, { maximumFractionDigits: 6 })}` : 'n/a');
|
|
891
|
+
const src0 = priceSource0 || 'n/a';
|
|
892
|
+
const src1 = priceSource1 || 'n/a';
|
|
893
|
+
console.log(`Prices: ${snap.meta.token0.symbol} ${fmt(token0USD)} (${src0}), ${snap.meta.token1.symbol} ${fmt(token1USD)} (${src1})`);
|
|
894
|
+
}
|
|
895
|
+
console.log(`Range: [${snap.meta.range.start}, ${snap.meta.range.end}] for ±${snap.meta.farPercent}%`);
|
|
896
|
+
console.log('');
|
|
897
|
+
const labels = percentBuckets.map((p) => `${p}%`);
|
|
898
|
+
console.log(`Buy cumulative (token1 in → price up):`);
|
|
899
|
+
labels.forEach((lab, i) => console.log(` +${lab.padEnd(4)}: ${fmtUSD(result.buyCumUSD[i])}`));
|
|
900
|
+
console.log(`Sell cumulative (token0 in → price down):`);
|
|
901
|
+
labels.forEach((lab, i) => console.log(` -${lab.padEnd(4)}: ${fmtUSD(result.sellCumUSD[i])}`));
|
|
902
|
+
console.log('');
|
|
903
|
+
console.log(`Headline Depth +2% : ${fmtUSD(result.depthPlus2USD)}`);
|
|
904
|
+
console.log(`Headline Depth -2% : ${fmtUSD(result.depthMinus2USD)}`);
|
|
905
|
+
if (!priceImpacts && usdSizes.length) {
|
|
906
|
+
console.log('');
|
|
907
|
+
console.log('Note: USD sizes provided but missing USD prices.');
|
|
908
|
+
console.log(' Pass --token0-usd and --token1-usd to enable price impact output.');
|
|
909
|
+
}
|
|
910
|
+
else if (priceImpacts) {
|
|
911
|
+
console.log('');
|
|
912
|
+
console.log('Price impact by USD size:');
|
|
913
|
+
usdSizes.forEach((u, i) => {
|
|
914
|
+
const buy = priceImpacts.buyPct[i];
|
|
915
|
+
const sell = priceImpacts.sellPct[i];
|
|
916
|
+
const fmt = (x) => `${x >= 0 ? '+' : ''}${x.toFixed(5)}%`;
|
|
917
|
+
console.log(` $${u.toLocaleString()}: buy ${fmt(buy)} sell ${fmt(sell)}`);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
// If we computed an 'after' snapshot, append a second pretty-printed section.
|
|
921
|
+
if (after) {
|
|
922
|
+
const snapA = after.snap;
|
|
923
|
+
const resultA = after.result;
|
|
924
|
+
const token0USDA = after.token0USD;
|
|
925
|
+
const token1USDA = after.token1USD;
|
|
926
|
+
const priceImpactsA = after.priceImpacts;
|
|
927
|
+
const usdSizesA = after.usdSizes;
|
|
928
|
+
console.log('');
|
|
929
|
+
console.log('--- After Withdraw ---');
|
|
930
|
+
console.log(`Tokens: ${snapA.meta.token0.symbol} (${snapA.meta.token0.id}) / ${snapA.meta.token1.symbol} (${snapA.meta.token1.id})`);
|
|
931
|
+
if (snapA.meta?.ethPriceUSD)
|
|
932
|
+
console.log(`ETH/USD: ${Number(snapA.meta.ethPriceUSD).toFixed(2)}`);
|
|
933
|
+
const feePctA = snapA.feePips / 10_000;
|
|
934
|
+
const feeBpsA = snapA.feePips / 100;
|
|
935
|
+
console.log(`Tick: ${snapA.tick} Fee: ${feePctA.toFixed(2)}% (${feeBpsA.toFixed(2)} bps)`);
|
|
936
|
+
if (token0USDA || token1USDA) {
|
|
937
|
+
const fmt = (v) => (v ? `$${v.toLocaleString(undefined, { maximumFractionDigits: 6 })}` : 'n/a');
|
|
938
|
+
const src0 = after.priceSource0 || 'n/a';
|
|
939
|
+
const src1 = after.priceSource1 || 'n/a';
|
|
940
|
+
console.log(`Prices: ${snapA.meta.token0.symbol} ${fmt(token0USDA)} (${src0}), ${snapA.meta.token1.symbol} ${fmt(token1USDA)} (${src1})`);
|
|
941
|
+
}
|
|
942
|
+
console.log(`Range: [${snapA.meta.range.start}, ${snapA.meta.range.end}] for ±${snapA.meta.farPercent}%`);
|
|
943
|
+
console.log('');
|
|
944
|
+
const labelsA = percentBuckets.map((p) => `${p}%`);
|
|
945
|
+
console.log(`Buy cumulative (token1 in → price up):`);
|
|
946
|
+
labelsA.forEach((lab, i) => console.log(` +${lab.padEnd(4)}: ${fmtUSD(resultA.buyCumUSD[i])}`));
|
|
947
|
+
console.log(`Sell cumulative (token0 in → price down):`);
|
|
948
|
+
labelsA.forEach((lab, i) => console.log(` -${lab.padEnd(4)}: ${fmtUSD(resultA.sellCumUSD[i])}`));
|
|
949
|
+
console.log('');
|
|
950
|
+
console.log(`Headline Depth +2% : ${fmtUSD(resultA.depthPlus2USD)}`);
|
|
951
|
+
console.log(`Headline Depth -2% : ${fmtUSD(resultA.depthMinus2USD)}`);
|
|
952
|
+
if (!priceImpactsA && usdSizesA.length) {
|
|
953
|
+
console.log('');
|
|
954
|
+
console.log('Note: USD sizes provided but missing USD prices.');
|
|
955
|
+
console.log(' Pass --token0-usd and --token1-usd to enable price impact output.');
|
|
956
|
+
}
|
|
957
|
+
else if (priceImpactsA) {
|
|
958
|
+
console.log('');
|
|
959
|
+
console.log('Price impact by USD size:');
|
|
960
|
+
usdSizesA.forEach((u, i) => {
|
|
961
|
+
const buy = priceImpactsA.buyPct[i];
|
|
962
|
+
const sell = priceImpactsA.sellPct[i];
|
|
963
|
+
const fmt = (x) => `${x >= 0 ? '+' : ''}${x.toFixed(5)}%`;
|
|
964
|
+
console.log(` $${u.toLocaleString()}: buy ${fmt(buy)} sell ${fmt(sell)}`);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
main().catch((err) => {
|
|
970
|
+
console.error(err?.stack || err?.message || String(err));
|
|
971
|
+
process.exit(1);
|
|
972
|
+
});
|
|
973
|
+
// --- Helpers for Tevm simulation ---
|
|
974
|
+
// ABIs come from wagmi CLI generated types
|
|
975
|
+
async function simulateVaultWithdraw({ client, vault, owner, withdrawPct, withdrawShares, debug = false, cliPool, viemChain }) {
|
|
976
|
+
if (!client)
|
|
977
|
+
throw new Error('simulateVaultWithdraw requires tevm client');
|
|
978
|
+
if (!vault)
|
|
979
|
+
throw new Error('Missing --vault');
|
|
980
|
+
if (!owner)
|
|
981
|
+
throw new Error('Missing --owner for withdraw simulation');
|
|
982
|
+
const vaultAddr = getAddress(vault);
|
|
983
|
+
const ownerAddr = getAddress(owner);
|
|
984
|
+
// Resolve vault + token details
|
|
985
|
+
let poolAddr;
|
|
986
|
+
try {
|
|
987
|
+
poolAddr = await client.readContract({ address: vaultAddr, abi: steerVaultAbi, functionName: 'pool' });
|
|
988
|
+
}
|
|
989
|
+
catch (e) {
|
|
990
|
+
throw new Error(`Failed to read vault.pool(): ${e?.message || e}`);
|
|
991
|
+
}
|
|
992
|
+
let vDec = 18;
|
|
993
|
+
try {
|
|
994
|
+
vDec = Number(await client.readContract({ address: vaultAddr, abi: steerVaultAbi, functionName: 'decimals' }));
|
|
995
|
+
}
|
|
996
|
+
catch (_) { }
|
|
997
|
+
const t0Addr = await client.readContract({ address: vaultAddr, abi: steerVaultAbi, functionName: 'token0' });
|
|
998
|
+
const t1Addr = await client.readContract({ address: vaultAddr, abi: steerVaultAbi, functionName: 'token1' });
|
|
999
|
+
const [t0Dec, t1Dec] = await Promise.all([
|
|
1000
|
+
client.readContract({ address: t0Addr, abi: erc20Abi, functionName: 'decimals' }),
|
|
1001
|
+
client.readContract({ address: t1Addr, abi: erc20Abi, functionName: 'decimals' }),
|
|
1002
|
+
]);
|
|
1003
|
+
let [t0Sym, t1Sym] = ['T0', 'T1'];
|
|
1004
|
+
try {
|
|
1005
|
+
t0Sym = await client.readContract({ address: t0Addr, abi: erc20Abi, functionName: 'symbol' });
|
|
1006
|
+
}
|
|
1007
|
+
catch (_) { }
|
|
1008
|
+
try {
|
|
1009
|
+
t1Sym = await client.readContract({ address: t1Addr, abi: erc20Abi, functionName: 'symbol' });
|
|
1010
|
+
}
|
|
1011
|
+
catch (_) { }
|
|
1012
|
+
const fmtUnits = (bn, dec) => {
|
|
1013
|
+
try {
|
|
1014
|
+
let v = BigInt(bn);
|
|
1015
|
+
const neg = v < 0n;
|
|
1016
|
+
if (neg)
|
|
1017
|
+
v = -v;
|
|
1018
|
+
const s = v.toString().padStart(Number(dec) + 1, '0');
|
|
1019
|
+
const i = s.slice(0, s.length - Number(dec));
|
|
1020
|
+
const f = s.slice(s.length - Number(dec)).replace(/0+$/, '').slice(0, 8);
|
|
1021
|
+
return (neg ? '-' : '') + i + (f ? '.' + f : '');
|
|
1022
|
+
}
|
|
1023
|
+
catch (_) {
|
|
1024
|
+
return String(bn);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const fmt = (bn, dec, sym) => `${fmtUnits(bn, dec)} ${sym || ''}`.trim();
|
|
1028
|
+
// Determine shares to withdraw
|
|
1029
|
+
let shares = null;
|
|
1030
|
+
if (withdrawShares != null) {
|
|
1031
|
+
try {
|
|
1032
|
+
shares = BigInt(String(withdrawShares));
|
|
1033
|
+
}
|
|
1034
|
+
catch (_) {
|
|
1035
|
+
throw new Error('--withdraw-shares must be a BigInt-compatible value');
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else if (Number.isFinite(withdrawPct)) {
|
|
1039
|
+
const bal = await client.readContract({ address: vaultAddr, abi: erc20Abi, functionName: 'balanceOf', args: [ownerAddr] });
|
|
1040
|
+
const pct = BigInt(Math.max(0, Math.min(100, Math.floor(Number(withdrawPct)))));
|
|
1041
|
+
shares = (BigInt(bal) * pct) / 100n;
|
|
1042
|
+
}
|
|
1043
|
+
const ownerShares = await client.readContract({ address: vaultAddr, abi: erc20Abi, functionName: 'balanceOf', args: [ownerAddr] });
|
|
1044
|
+
const totalShares = await client.readContract({ address: vaultAddr, abi: erc20Abi, functionName: 'totalSupply' });
|
|
1045
|
+
if (debug) {
|
|
1046
|
+
const poolWarn = cliPool && cliPool.toLowerCase() !== poolAddr.toLowerCase() ? ` (CLI pool=${cliPool})` : '';
|
|
1047
|
+
console.error(`[withdraw] vault=${vaultAddr} pool=${poolAddr}${poolWarn}`);
|
|
1048
|
+
console.error(`[withdraw] owner=${ownerAddr} ownerShares=${fmt(ownerShares, vDec, 'shares')} totalSupply=${fmt(totalShares, vDec, 'shares')}`);
|
|
1049
|
+
console.error(`[withdraw] planned shares=${fmt(shares ?? 0n, vDec, 'shares')} (${Number(withdrawPct ?? 0)}%)`);
|
|
1050
|
+
}
|
|
1051
|
+
if (!shares || shares <= 0n) {
|
|
1052
|
+
if (debug)
|
|
1053
|
+
console.error('[withdraw] no shares to withdraw; skipping');
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
// Fund owner for gas and impersonate
|
|
1057
|
+
try {
|
|
1058
|
+
await client.setBalance?.({ address: ownerAddr, value: parseEther('0.05') });
|
|
1059
|
+
}
|
|
1060
|
+
catch (_) { }
|
|
1061
|
+
try {
|
|
1062
|
+
await client.impersonateAccount?.({ address: ownerAddr });
|
|
1063
|
+
}
|
|
1064
|
+
catch (_) { }
|
|
1065
|
+
// Balances before
|
|
1066
|
+
const [o0b, o1b, v0b, v1b] = await Promise.all([
|
|
1067
|
+
client.readContract({ address: t0Addr, abi: erc20Abi, functionName: 'balanceOf', args: [ownerAddr] }),
|
|
1068
|
+
client.readContract({ address: t1Addr, abi: erc20Abi, functionName: 'balanceOf', args: [ownerAddr] }),
|
|
1069
|
+
client.readContract({ address: t0Addr, abi: erc20Abi, functionName: 'balanceOf', args: [vaultAddr] }),
|
|
1070
|
+
client.readContract({ address: t1Addr, abi: erc20Abi, functionName: 'balanceOf', args: [vaultAddr] }),
|
|
1071
|
+
]);
|
|
1072
|
+
if (debug) {
|
|
1073
|
+
console.error(`[withdraw] owner balances before: ${fmt(o0b, t0Dec, t0Sym)}, ${fmt(o1b, t1Dec, t1Sym)}`);
|
|
1074
|
+
console.error(`[withdraw] vault balances before: ${fmt(v0b, t0Dec, t0Sym)}, ${fmt(v1b, t1Dec, t1Sym)}`);
|
|
1075
|
+
}
|
|
1076
|
+
// Build and send withdraw tx
|
|
1077
|
+
const data = encodeFunctionData({ abi: steerVaultAbi, functionName: 'withdraw', args: [shares, 0n, 0n, ownerAddr] });
|
|
1078
|
+
try {
|
|
1079
|
+
// Provide chain (common) to satisfy viem wallet action requirements
|
|
1080
|
+
const chain = viemChain || await resolveViemChainFromClient(client);
|
|
1081
|
+
const hash = await viemSendTransaction(client, { account: ownerAddr, to: vaultAddr, data, chain });
|
|
1082
|
+
if (debug)
|
|
1083
|
+
console.error(`[withdraw] tx sent: ${hash}`);
|
|
1084
|
+
}
|
|
1085
|
+
catch (e) {
|
|
1086
|
+
if (debug)
|
|
1087
|
+
console.error(`[withdraw] sendTransaction error: ${e?.message || e}`);
|
|
1088
|
+
throw e;
|
|
1089
|
+
}
|
|
1090
|
+
await client.mine?.({ blocks: 1 });
|
|
1091
|
+
// Balances after
|
|
1092
|
+
const [o0a, o1a, v0a, v1a] = await Promise.all([
|
|
1093
|
+
client.readContract({ address: t0Addr, abi: erc20Abi, functionName: 'balanceOf', args: [ownerAddr] }),
|
|
1094
|
+
client.readContract({ address: t1Addr, abi: erc20Abi, functionName: 'balanceOf', args: [ownerAddr] }),
|
|
1095
|
+
client.readContract({ address: t0Addr, abi: erc20Abi, functionName: 'balanceOf', args: [vaultAddr] }),
|
|
1096
|
+
client.readContract({ address: t1Addr, abi: erc20Abi, functionName: 'balanceOf', args: [vaultAddr] }),
|
|
1097
|
+
]);
|
|
1098
|
+
const d0 = BigInt(o0a) - BigInt(o0b);
|
|
1099
|
+
const d1 = BigInt(o1a) - BigInt(o1b);
|
|
1100
|
+
if (debug) {
|
|
1101
|
+
console.error(`[withdraw] owner balances after : ${fmt(o0a, t0Dec, t0Sym)}, ${fmt(o1a, t1Dec, t1Sym)}`);
|
|
1102
|
+
console.error(`[withdraw] vault balances after : ${fmt(v0a, t0Dec, t0Sym)}, ${fmt(v1a, t1Dec, t1Sym)}`);
|
|
1103
|
+
console.error(`[withdraw] owner deltas: ${fmt(d0, t0Dec, t0Sym)}, ${fmt(d1, t1Dec, t1Sym)}`);
|
|
1104
|
+
}
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
// Resolve a tevm/common chain object for a given RPC (by chainId)
|
|
1108
|
+
async function resolveTevmCommon(rpcUrl) {
|
|
1109
|
+
try {
|
|
1110
|
+
if (!rpcUrl)
|
|
1111
|
+
return undefined;
|
|
1112
|
+
const pc = createPublicClient({ transport: viemHttp(rpcUrl) });
|
|
1113
|
+
const id = await pc.getChainId();
|
|
1114
|
+
for (const v of Object.values(tevmCommon)) {
|
|
1115
|
+
if (v && typeof v === 'object' && 'id' in v && v.id === id)
|
|
1116
|
+
return v;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
catch (_) { }
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
// Resolve tevm/common by introspecting an existing client
|
|
1123
|
+
async function resolveTevmCommonFromClient(client) {
|
|
1124
|
+
try {
|
|
1125
|
+
const id = await client.getChainId();
|
|
1126
|
+
for (const v of Object.values(tevmCommon)) {
|
|
1127
|
+
if (v && typeof v === 'object' && 'id' in v && v.id === id)
|
|
1128
|
+
return v;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch (_) { }
|
|
1132
|
+
return undefined;
|
|
1133
|
+
}
|
|
1134
|
+
// Resolve a viem Chain object for a given RPC (by chainId)
|
|
1135
|
+
async function resolveViemChain(rpcUrl) {
|
|
1136
|
+
try {
|
|
1137
|
+
if (!rpcUrl)
|
|
1138
|
+
return undefined;
|
|
1139
|
+
const pc = createPublicClient({ transport: viemHttp(rpcUrl) });
|
|
1140
|
+
const id = await pc.getChainId();
|
|
1141
|
+
for (const v of Object.values(viemChains)) {
|
|
1142
|
+
if (v && typeof v === 'object' && 'id' in v && v.id === id)
|
|
1143
|
+
return v;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
catch (_) { }
|
|
1147
|
+
return undefined;
|
|
1148
|
+
}
|
|
1149
|
+
// Resolve viem Chain by introspecting an existing client
|
|
1150
|
+
async function resolveViemChainFromClient(client) {
|
|
1151
|
+
try {
|
|
1152
|
+
const id = await client.getChainId();
|
|
1153
|
+
for (const v of Object.values(viemChains)) {
|
|
1154
|
+
if (v && typeof v === 'object' && 'id' in v && v.id === id)
|
|
1155
|
+
return v;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
catch (_) { }
|
|
1159
|
+
return undefined;
|
|
1160
|
+
}
|