@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.
@@ -0,0 +1,150 @@
1
+ import { getAddress, encodeFunctionData, parseEther, createPublicClient, http as viemHttp } from 'viem';
2
+ import { sendTransaction as viemSendTransaction } from 'viem/actions';
3
+ import * as tevmCommon from 'tevm/common';
4
+ import * as viemChains from 'viem/chains';
5
+ import { erc20Abi, steerVaultAbi } from '../wagmi/generated.js';
6
+ const ERC20_ABI = erc20Abi;
7
+ const VAULT_ABI = steerVaultAbi;
8
+ export async function simulateVaultWithdraw(params) {
9
+ const { client, vault, owner, withdrawPct, withdrawShares, debug = false, cliPool, viemChain } = params;
10
+ if (!client)
11
+ throw new Error('simulateVaultWithdraw requires tevm client');
12
+ if (!vault)
13
+ throw new Error('Missing vault');
14
+ if (!owner)
15
+ throw new Error('Missing owner for withdraw simulation');
16
+ const vaultAddr = getAddress(vault);
17
+ const ownerAddr = getAddress(owner);
18
+ let poolAddr;
19
+ try {
20
+ poolAddr = await client.readContract({ address: vaultAddr, abi: VAULT_ABI, functionName: 'pool' });
21
+ }
22
+ catch (e) {
23
+ throw new Error(`Failed to read vault.pool(): ${e?.message || e}`);
24
+ }
25
+ let vDec = 18;
26
+ try {
27
+ vDec = Number(await client.readContract({ address: vaultAddr, abi: VAULT_ABI, functionName: 'decimals' }));
28
+ }
29
+ catch { }
30
+ const t0Addr = await client.readContract({ address: vaultAddr, abi: VAULT_ABI, functionName: 'token0' });
31
+ const t1Addr = await client.readContract({ address: vaultAddr, abi: VAULT_ABI, functionName: 'token1' });
32
+ const [t0Dec, t1Dec] = await Promise.all([
33
+ client.readContract({ address: t0Addr, abi: ERC20_ABI, functionName: 'decimals' }),
34
+ client.readContract({ address: t1Addr, abi: ERC20_ABI, functionName: 'decimals' }),
35
+ ]);
36
+ let [t0Sym, t1Sym] = ['T0', 'T1'];
37
+ try {
38
+ t0Sym = await client.readContract({ address: t0Addr, abi: ERC20_ABI, functionName: 'symbol' });
39
+ }
40
+ catch { }
41
+ try {
42
+ t1Sym = await client.readContract({ address: t1Addr, abi: ERC20_ABI, functionName: 'symbol' });
43
+ }
44
+ catch { }
45
+ const fmtUnits = (bn, dec) => {
46
+ try {
47
+ let v = BigInt(bn);
48
+ const neg = v < 0n;
49
+ if (neg)
50
+ v = -v;
51
+ const s = v.toString().padStart(Number(dec) + 1, '0');
52
+ const i = s.slice(0, s.length - Number(dec));
53
+ const f = s.slice(s.length - Number(dec)).replace(/0+$/, '').slice(0, 8);
54
+ return (neg ? '-' : '') + i + (f ? '.' + f : '');
55
+ }
56
+ catch {
57
+ return String(bn);
58
+ }
59
+ };
60
+ const fmt = (bn, dec, sym) => `${fmtUnits(bn, dec)} ${sym}`;
61
+ let shares = withdrawShares != null ? BigInt(withdrawShares) : undefined;
62
+ if (shares == null && withdrawPct != null) {
63
+ const bal = await client.readContract({ address: vaultAddr, abi: ERC20_ABI, functionName: 'balanceOf', args: [ownerAddr] });
64
+ const pct = BigInt(Math.max(0, Math.min(100, Math.floor(Number(withdrawPct)))));
65
+ shares = (BigInt(bal) * pct) / 100n;
66
+ }
67
+ const ownerShares = await client.readContract({ address: vaultAddr, abi: ERC20_ABI, functionName: 'balanceOf', args: [ownerAddr] });
68
+ const totalShares = await client.readContract({ address: vaultAddr, abi: ERC20_ABI, functionName: 'totalSupply' });
69
+ if (debug) {
70
+ const poolWarn = cliPool && cliPool.toLowerCase() !== poolAddr.toLowerCase() ? ` (CLI pool=${cliPool})` : '';
71
+ console.error(`[withdraw] vault=${vaultAddr} pool=${poolAddr}${poolWarn}`);
72
+ console.error(`[withdraw] owner=${ownerAddr} ownerShares=${fmt(ownerShares, vDec, 'shares')} totalSupply=${fmt(totalShares, vDec, 'shares')}`);
73
+ console.error(`[withdraw] planned shares=${fmt(shares ?? 0n, vDec, 'shares')} (${Number(withdrawPct ?? 0)}%)`);
74
+ }
75
+ if (!shares || shares <= 0n) {
76
+ if (debug)
77
+ console.error('[withdraw] no shares to withdraw; skipping');
78
+ return false;
79
+ }
80
+ try {
81
+ await client.setBalance({ address: ownerAddr, value: parseEther('0.05') });
82
+ }
83
+ catch { }
84
+ try {
85
+ await client.impersonateAccount({ address: ownerAddr });
86
+ }
87
+ catch { }
88
+ const [o0b, o1b, v0b, v1b] = await Promise.all([
89
+ client.readContract({ address: t0Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [ownerAddr] }),
90
+ client.readContract({ address: t1Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [ownerAddr] }),
91
+ client.readContract({ address: t0Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [vaultAddr] }),
92
+ client.readContract({ address: t1Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [vaultAddr] }),
93
+ ]);
94
+ if (debug) {
95
+ console.error(`[withdraw] owner balances before: ${fmt(o0b, t0Dec, t0Sym)}, ${fmt(o1b, t1Dec, t1Sym)}`);
96
+ console.error(`[withdraw] vault balances before: ${fmt(v0b, t0Dec, t0Sym)}, ${fmt(v1b, t1Dec, t1Sym)}`);
97
+ }
98
+ const data = encodeFunctionData({ abi: VAULT_ABI, functionName: 'withdraw', args: [shares, 0n, 0n, ownerAddr] });
99
+ try {
100
+ const chain = viemChain || await resolveViemChainFromClient(client);
101
+ const hash = await viemSendTransaction(client, { account: ownerAddr, to: vaultAddr, data, chain });
102
+ if (debug)
103
+ console.error(`[withdraw] tx sent: ${hash}`);
104
+ }
105
+ catch (e) {
106
+ if (debug)
107
+ console.error(`[withdraw] sendTransaction error: ${e?.message || e}`);
108
+ throw e;
109
+ }
110
+ await client.mine?.({ blocks: 1 });
111
+ const [o0a, o1a, v0a, v1a] = await Promise.all([
112
+ client.readContract({ address: t0Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [ownerAddr] }),
113
+ client.readContract({ address: t1Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [ownerAddr] }),
114
+ client.readContract({ address: t0Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [vaultAddr] }),
115
+ client.readContract({ address: t1Addr, abi: ERC20_ABI, functionName: 'balanceOf', args: [vaultAddr] }),
116
+ ]);
117
+ const d0 = BigInt(o0a) - BigInt(o0b);
118
+ const d1 = BigInt(o1a) - BigInt(o1b);
119
+ if (debug) {
120
+ console.error(`[withdraw] owner balances after : ${fmt(o0a, t0Dec, t0Sym)}, ${fmt(o1a, t1Dec, t1Sym)}`);
121
+ console.error(`[withdraw] vault balances after : ${fmt(v0a, t0Dec, t0Sym)}, ${fmt(v1a, t1Dec, t1Sym)}`);
122
+ console.error(`[withdraw] owner deltas: ${fmt(d0, t0Dec, t0Sym)}, ${fmt(d1, t1Dec, t1Sym)}`);
123
+ }
124
+ return true;
125
+ }
126
+ export async function resolveViemChainFromClient(client) {
127
+ try {
128
+ const id = await client.getChainId();
129
+ for (const v of Object.values(viemChains)) {
130
+ if (v && typeof v === 'object' && 'id' in v && v.id === id)
131
+ return v;
132
+ }
133
+ }
134
+ catch { }
135
+ return undefined;
136
+ }
137
+ export async function resolveTevmCommon(rpcUrl) {
138
+ try {
139
+ if (!rpcUrl)
140
+ return undefined;
141
+ const pc = createPublicClient({ transport: viemHttp(rpcUrl) });
142
+ const id = await pc.getChainId();
143
+ for (const v of Object.values(tevmCommon)) {
144
+ if (v && typeof v === 'object' && 'id' in v && v.id === id)
145
+ return v;
146
+ }
147
+ }
148
+ catch { }
149
+ return undefined;
150
+ }
package/dist/api.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { computeMarketDepthBands } from './core/depth.js';
2
+ export type AnalyzeSource = 'tevm' | 'subgraph' | 'auto';
3
+ export type AnalyzeOptions = {
4
+ poolAddress: string;
5
+ source?: AnalyzeSource;
6
+ rpcUrl?: string;
7
+ subgraphUrl?: string;
8
+ percentBuckets?: number[];
9
+ usdSizes?: number[];
10
+ token0USD?: number;
11
+ token1USD?: number;
12
+ assumeStable?: 0 | 1;
13
+ prices?: 'reserve' | 'dexscreener' | 'llama' | 'coingecko' | 'auto';
14
+ reserveLimit?: number;
15
+ debug?: boolean;
16
+ withdraw?: {
17
+ vault: string;
18
+ owner: string;
19
+ withdrawPct?: number;
20
+ withdrawShares?: string | number | bigint;
21
+ };
22
+ };
23
+ export type AnalyzeResult = {
24
+ before: SingleAnalysis;
25
+ after?: SingleAnalysis;
26
+ };
27
+ export type SingleAnalysis = {
28
+ pool: string;
29
+ tokens: {
30
+ token0: {
31
+ id?: string;
32
+ symbol?: string;
33
+ };
34
+ token1: {
35
+ id?: string;
36
+ symbol?: string;
37
+ };
38
+ };
39
+ ethPriceUSD?: number;
40
+ tick: number;
41
+ sqrtPriceX96: string;
42
+ liquidity: string;
43
+ feePips: number;
44
+ range: {
45
+ start: number;
46
+ end: number;
47
+ };
48
+ percentBuckets: number[];
49
+ prices: {
50
+ token0?: {
51
+ address?: string;
52
+ symbol?: string;
53
+ usd?: number;
54
+ source?: string;
55
+ };
56
+ token1?: {
57
+ address?: string;
58
+ symbol?: string;
59
+ usd?: number;
60
+ source?: string;
61
+ };
62
+ };
63
+ buyCumToken1: string[];
64
+ sellCumToken0: string[];
65
+ metrics: ReturnType<typeof computeMarketDepthBands>;
66
+ priceImpacts?: {
67
+ usdSizes: number[];
68
+ buyPct: number[];
69
+ sellPct: number[];
70
+ };
71
+ };
72
+ export declare function analyzePool(opts: AnalyzeOptions): Promise<AnalyzeResult>;
package/dist/api.js ADDED
@@ -0,0 +1,180 @@
1
+ import { createMemoryClient, http as tevmHttp } from 'tevm';
2
+ import { computeMarketDepthBands, computePriceImpactsBySizes } from './core/depth.js';
3
+ import { fetchPoolSnapshot as fetchFromSubgraph } from './adapters/uniswapv3-subgraph.js';
4
+ import { fetchPoolSnapshotViem } from './adapters/onchain.js';
5
+ import { fetchTokenUSDPrices } from './prices.js';
6
+ import { resolveTevmCommon, simulateVaultWithdraw } from './adapters/withdraw.js';
7
+ export async function analyzePool(opts) {
8
+ const { poolAddress, source = 'auto', rpcUrl, subgraphUrl, percentBuckets = [1, 2, 5, 10], usdSizes = [], token0USD: token0Override, token1USD: token1Override, assumeStable, prices = 'auto', reserveLimit = 100, debug = false, withdraw, } = opts;
9
+ // Decide source
10
+ let used;
11
+ let client = null;
12
+ if (source === 'tevm' || (source === 'auto' && rpcUrl)) {
13
+ if (!rpcUrl)
14
+ throw new Error('analyzePool: rpcUrl required for tevm source');
15
+ const common = await resolveTevmCommon(rpcUrl);
16
+ client = createMemoryClient({ fork: { transport: tevmHttp(rpcUrl), blockTag: 'latest' }, loggingLevel: 'error', ...(common ? { common } : {}) });
17
+ if (client.tevmReady)
18
+ await client.tevmReady();
19
+ used = 'tevm';
20
+ }
21
+ else {
22
+ used = 'subgraph';
23
+ }
24
+ // Snapshot
25
+ const snap = used === 'tevm'
26
+ ? await fetchPoolSnapshotViem({ poolAddress, rpcUrl: rpcUrl, percentBuckets, client })
27
+ : await fetchFromSubgraph({ poolAddress, percentBuckets, subgraphUrl });
28
+ const before = await computeSnapshotMetrics({
29
+ poolAddress,
30
+ args: { token0USD: token0Override, token1USD: token1Override, assumeStable, usdSizes, prices, reserveLimit, debug, rpcUrl },
31
+ snap,
32
+ percentBuckets,
33
+ });
34
+ // Optional withdraw simulation on TEVM
35
+ let after;
36
+ if (used === 'tevm' && withdraw?.vault && withdraw?.owner && (Number.isFinite(withdraw.withdrawPct) || withdraw.withdrawShares != null)) {
37
+ await simulateVaultWithdraw({ client, vault: withdraw.vault, owner: withdraw.owner, withdrawPct: withdraw.withdrawPct, withdrawShares: withdraw.withdrawShares, debug });
38
+ const snap2 = await fetchPoolSnapshotViem({ poolAddress, client, percentBuckets });
39
+ after = await computeSnapshotMetrics({ poolAddress, args: { token0USD: token0Override, token1USD: token1Override, assumeStable, usdSizes, prices, reserveLimit, debug, rpcUrl }, snap: snap2, percentBuckets });
40
+ }
41
+ return { before, after };
42
+ }
43
+ async function computeSnapshotMetrics({ poolAddress, args, snap, percentBuckets }) {
44
+ const usd0 = Number.isFinite(args.token0USD) ? args.token0USD : snap.token0.usdPrice;
45
+ const usd1 = Number.isFinite(args.token1USD) ? args.token1USD : snap.token1.usdPrice;
46
+ let token0USD = usd0;
47
+ let token1USD = usd1;
48
+ const dec0 = snap.token0.decimals;
49
+ const dec1 = snap.token1.decimals;
50
+ const price1Per0 = Math.pow(1.0001, snap.tick) * Math.pow(10, dec0 - dec1);
51
+ if (!usd0 && usd1)
52
+ token0USD = usd1 * price1Per0;
53
+ if (usd0 && !usd1)
54
+ token1USD = usd0 / price1Per0;
55
+ let priceSource0 = usd0 ? 'override' : undefined;
56
+ let priceSource1 = usd1 ? 'override' : undefined;
57
+ if ((!token0USD || !token1USD)) {
58
+ try {
59
+ const addrs = [snap.meta?.token0?.id || snap.token0.id, snap.meta?.token1?.id || snap.token1.id].map((x) => x.toLowerCase());
60
+ let chainIdForPrices;
61
+ if (args.rpcUrl) {
62
+ try {
63
+ const clientCID = createMemoryClient({ fork: { transport: tevmHttp(args.rpcUrl), blockTag: 'latest' }, loggingLevel: 'error' });
64
+ if (clientCID.tevmReady)
65
+ await clientCID.tevmReady();
66
+ chainIdForPrices = await clientCID.getChainId();
67
+ }
68
+ catch { }
69
+ }
70
+ if (args.debug)
71
+ console.error(`[api] price fetch: addrs=${addrs.join(',')} source=${args.prices} rpc=${args.rpcUrl ? 'yes' : 'no'} chainId=${chainIdForPrices ?? 'n/a'}`);
72
+ const { byAddress } = await fetchTokenUSDPrices({ addresses: addrs, source: args.prices, chainId: chainIdForPrices, rpcUrl: args.rpcUrl, debug: !!args.debug, reserveLimit: args.reserveLimit });
73
+ const p0 = byAddress[addrs[0]];
74
+ const p1 = byAddress[addrs[1]];
75
+ if (!token0USD && p0) {
76
+ token0USD = p0.price;
77
+ priceSource0 = p0.source;
78
+ }
79
+ if (!token1USD && p1) {
80
+ token1USD = p1.price;
81
+ priceSource1 = p1.source;
82
+ }
83
+ if (token0USD && !token1USD) {
84
+ token1USD = token0USD / price1Per0;
85
+ if (!priceSource1)
86
+ priceSource1 = 'derived';
87
+ }
88
+ if (!token0USD && token1USD) {
89
+ token0USD = token1USD * price1Per0;
90
+ if (!priceSource0)
91
+ priceSource0 = 'derived';
92
+ }
93
+ }
94
+ catch { }
95
+ }
96
+ if (!priceSource0 && usd0)
97
+ priceSource0 = 'subgraph';
98
+ if (!priceSource1 && usd1)
99
+ priceSource1 = 'subgraph';
100
+ if ((!token0USD || !token1USD)) {
101
+ if (args.assumeStable === 0 || /USD|USDC|USDT|DAI/i.test((snap.meta?.token0?.symbol || ''))) {
102
+ if (!token0USD) {
103
+ token0USD = 1;
104
+ priceSource0 = 'inferred';
105
+ }
106
+ if (!token1USD) {
107
+ token1USD = 1 / price1Per0;
108
+ priceSource1 = 'inferred';
109
+ }
110
+ }
111
+ else if (args.assumeStable === 1 || /USD|USDC|USDT|DAI/i.test((snap.meta?.token1?.symbol || ''))) {
112
+ if (!token1USD) {
113
+ token1USD = 1;
114
+ priceSource1 = 'inferred';
115
+ }
116
+ if (!token0USD) {
117
+ token0USD = price1Per0;
118
+ priceSource0 = 'inferred';
119
+ }
120
+ }
121
+ }
122
+ if (!priceSource0 && token0USD)
123
+ priceSource0 = 'inferred';
124
+ if (!priceSource1 && token1USD)
125
+ priceSource1 = 'inferred';
126
+ const metrics = computeMarketDepthBands({
127
+ sqrtPriceX96: snap.sqrtPriceX96,
128
+ tick: snap.tick,
129
+ liquidity: snap.liquidity,
130
+ feePips: snap.feePips,
131
+ ticks: snap.ticks,
132
+ token0: { decimals: snap.token0.decimals, usdPrice: token0USD || 0 },
133
+ token1: { decimals: snap.token1.decimals, usdPrice: token1USD || 0 },
134
+ percentBuckets,
135
+ });
136
+ const usdSizes = (args.usdSizes || []);
137
+ const usdToRaw = (usd, usdPrice, decimals) => {
138
+ if (!usdPrice || usdPrice <= 0)
139
+ return 0n;
140
+ const micros = Math.round(usd * 1e6);
141
+ const usdMicrosPrice = Math.round(usdPrice * 1e6);
142
+ if (usdMicrosPrice <= 0)
143
+ return 0n;
144
+ const scale = 10n ** BigInt(decimals);
145
+ return (BigInt(micros) * scale) / BigInt(usdMicrosPrice);
146
+ };
147
+ let priceImpacts;
148
+ if (usdSizes.length && token0USD && token1USD) {
149
+ const buySizesToken1 = usdSizes.map((u) => usdToRaw(u, token1USD, snap.token1.decimals));
150
+ const sellSizesToken0 = usdSizes.map((u) => usdToRaw(u, token0USD, snap.token0.decimals));
151
+ priceImpacts = computePriceImpactsBySizes({
152
+ sqrtPriceX96: snap.sqrtPriceX96,
153
+ tick: snap.tick,
154
+ liquidity: snap.liquidity,
155
+ feePips: snap.feePips,
156
+ ticks: snap.ticks,
157
+ buySizesToken1,
158
+ sellSizesToken0,
159
+ });
160
+ }
161
+ return {
162
+ pool: snap.meta?.poolId ?? poolAddress,
163
+ tokens: { token0: snap.meta?.token0 ?? {}, token1: snap.meta?.token1 ?? {} },
164
+ ethPriceUSD: snap.meta?.ethPriceUSD,
165
+ tick: snap.tick,
166
+ sqrtPriceX96: String(snap.sqrtPriceX96),
167
+ liquidity: String(snap.liquidity),
168
+ feePips: snap.feePips,
169
+ range: snap.meta?.range ?? { start: snap.tick, end: snap.tick },
170
+ percentBuckets,
171
+ prices: {
172
+ token0: { address: snap.meta?.token0?.id, symbol: snap.meta?.token0?.symbol, usd: token0USD, source: priceSource0 },
173
+ token1: { address: snap.meta?.token1?.id, symbol: snap.meta?.token1?.symbol, usd: token1USD, source: priceSource1 },
174
+ },
175
+ buyCumToken1: metrics.buyCumToken1.map((x) => String(x)),
176
+ sellCumToken0: metrics.sellCumToken0.map((x) => String(x)),
177
+ metrics,
178
+ priceImpacts: priceImpacts ? { usdSizes, buyPct: priceImpacts.buyPct, sellPct: priceImpacts.sellPct } : undefined,
179
+ };
180
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};