|
1 | | -const { ethers } = require('ethers') |
2 | | -const BigNumber = require('bignumber.js') |
| 1 | +const { request, gql } = require('graphql-request') |
3 | 2 | const utils = require('../utils') |
4 | 3 |
|
5 | 4 | const chain = 'unit0' |
6 | | - |
7 | 5 | const config = { |
8 | | - factory: '0xcF3Ee60d29531B668Ae89FD3577E210082Da220b', |
9 | | - fromBlock: 2291892, |
10 | | - blockTime: 1, |
11 | 6 | uiBase: 'https://koalaswap.app', |
12 | | - rpc: 'https://rpc.unit0.dev', |
13 | | -} |
14 | | - |
15 | | -const provider = new ethers.providers.JsonRpcProvider(config.rpc) |
16 | | - |
17 | | -const factoryIface = new ethers.utils.Interface([ |
18 | | - 'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)', |
19 | | -]) |
20 | | - |
21 | | -const poolIface = new ethers.utils.Interface([ |
22 | | - 'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)', |
23 | | -]) |
24 | | - |
25 | | -const erc20Abi = [ |
26 | | - 'function balanceOf(address) view returns (uint256)', |
27 | | - 'function decimals() view returns (uint8)', |
28 | | - 'function symbol() view returns (string)', |
29 | | -] |
30 | | - |
31 | | -async function getTokenInfo(token, poolAddress) { |
32 | | - const c = new ethers.Contract(token, erc20Abi, provider) |
33 | | - let balance = '0', |
34 | | - decimals = 18, |
35 | | - symbol = token.slice(0, 6) |
36 | | - |
37 | | - try { |
38 | | - balance = (await c.balanceOf(poolAddress)).toString() |
39 | | - } catch (_) {} |
40 | | - try { |
41 | | - decimals = await c.decimals() |
42 | | - } catch (_) {} |
43 | | - try { |
44 | | - symbol = await c.symbol() |
45 | | - } catch (_) {} |
46 | | - |
47 | | - return { balance, decimals, symbol } |
| 7 | + subgraph: 'https://graph-unit-zero.umerge.org/subgraphs/name/koalaswap-v3-unit-zero/', |
48 | 8 | } |
49 | 9 |
|
50 | | -async function getOnchainPrice(tokenA, tokenB, poolAddress, decimalsA, decimalsB, priceBUSD) { |
51 | | - try { |
52 | | - const iface = new ethers.utils.Interface(['function slot0() view returns (bytes)']) |
53 | | - const selector = iface.getSighash('slot0') |
54 | | - const raw = await provider.call({ to: poolAddress, data: selector }) |
55 | | - |
56 | | - const sqrtPriceX96 = ethers.BigNumber.from('0x' + raw.slice(2, 66)) |
57 | | - |
58 | | - const ratio = new BigNumber(sqrtPriceX96.toString()) |
59 | | - .pow(2) |
60 | | - .div(new BigNumber(2).pow(192)) |
61 | | - .times(new BigNumber(10).pow(decimalsB - decimalsA)) |
62 | | - |
63 | | - const priceAUSD = ratio.times(priceBUSD) |
64 | | - return priceAUSD.toNumber() |
65 | | - } catch (e) { |
66 | | - console.warn('⚠️ On-chain price fallback failed for', tokenA, 'in pool', poolAddress, e.message) |
67 | | - return 0 |
| 10 | +const POOLS_QUERY = gql` |
| 11 | + query GetPools($first: Int!, $skip: Int!) { |
| 12 | + pools(first: $first, skip: $skip, orderBy: totalValueLockedUSD, orderDirection: desc) { |
| 13 | + id |
| 14 | + token0 { id symbol } |
| 15 | + token1 { id symbol } |
| 16 | + totalValueLockedUSD |
| 17 | + volumeUSD |
| 18 | + feesUSD |
| 19 | + feeTier |
| 20 | + } |
68 | 21 | } |
| 22 | +` |
| 23 | + |
| 24 | +async function getAllPoolsFromGraph() { |
| 25 | + const pageSize = 100 |
| 26 | + let skip = 0 |
| 27 | + let results = [] |
| 28 | + |
| 29 | + while (true) { |
| 30 | + const data = await request(config.subgraph, POOLS_QUERY, { first: pageSize, skip }) |
| 31 | + if (!data.pools || data.pools.length === 0) break |
| 32 | + results = results.concat(data.pools) |
| 33 | + skip += pageSize |
| 34 | + } |
| 35 | + return results |
69 | 36 | } |
70 | 37 |
|
71 | 38 | async function getPools() { |
72 | | - const logs = await provider.getLogs({ |
73 | | - address: config.factory, |
74 | | - fromBlock: config.fromBlock, |
75 | | - toBlock: 'latest', |
76 | | - topics: [factoryIface.getEventTopic('PoolCreated')], |
77 | | - }) |
78 | | - |
79 | | - const pools = logs.map((log) => { |
80 | | - const parsed = factoryIface.parseLog(log) |
81 | | - return { |
82 | | - token0: parsed.args.token0, |
83 | | - token1: parsed.args.token1, |
84 | | - fee: parsed.args.fee, |
85 | | - pool: parsed.args.pool, |
86 | | - } |
87 | | - }) |
88 | | - |
89 | | - const dataPools = [] |
90 | | - |
91 | | - for (const p of pools) { |
92 | | - const [t0, t1] = await Promise.all([ |
93 | | - getTokenInfo(p.token0, p.pool), |
94 | | - getTokenInfo(p.token1, p.pool), |
95 | | - ]) |
96 | | - |
97 | | - const prices = await utils.getPrices([p.token0, p.token1], chain) |
98 | | - let price0 = prices.pricesByAddress[p.token0.toLowerCase()] ?? 0 |
99 | | - let price1 = prices.pricesByAddress[p.token1.toLowerCase()] ?? 0 |
100 | | - |
101 | | - if (price0 === 0 && price1 > 0) { |
102 | | - price0 = await getOnchainPrice(p.token0, p.token1, p.pool, t0.decimals, t1.decimals, price1) |
103 | | - } else if (price1 === 0 && price0 > 0) { |
104 | | - price1 = await getOnchainPrice(p.token1, p.token0, p.pool, t1.decimals, t0.decimals, price0) |
105 | | - } |
106 | | - |
107 | | - if (price0 === 0 && price1 === 0) continue |
108 | | - |
109 | | - const tvl0 = new BigNumber(t0.balance).div(`1e${t0.decimals}`).times(price0) |
110 | | - const tvl1 = new BigNumber(t1.balance).div(`1e${t1.decimals}`).times(price1) |
111 | | - const tvl = tvl0.plus(tvl1) |
| 39 | + const pools = await getAllPoolsFromGraph() |
112 | 40 |
|
113 | | - // считаем volume/fee |
114 | | - let totalFee0 = 0n |
115 | | - let totalFee1 = 0n |
116 | | - try { |
117 | | - const currentBlock = await provider.getBlockNumber() |
118 | | - const fromBlock = Math.max( |
119 | | - currentBlock - Math.floor((24 * 3600) / config.blockTime), |
120 | | - config.fromBlock, |
121 | | - ) |
| 41 | + const dataPools = pools.map((p) => { |
| 42 | + const tvlUsd = Number(p.totalValueLockedUSD || 0) |
| 43 | + const volumeUsd1d = Number(p.volumeUSD || 0) |
| 44 | + const feesUsd1d = Number(p.feesUSD || 0) |
122 | 45 |
|
123 | | - const swapLogs = await provider.getLogs({ |
124 | | - address: p.pool, |
125 | | - fromBlock, |
126 | | - toBlock: currentBlock, |
127 | | - topics: [poolIface.getEventTopic('Swap')], |
128 | | - }) |
| 46 | + const apr = tvlUsd > 0 ? (feesUsd1d / tvlUsd) * 365 : 0 |
| 47 | + const apyBase = apr * 100 |
129 | 48 |
|
130 | | - for (const log of swapLogs) { |
131 | | - const args = poolIface.parseLog(log).args |
132 | | - const amt0 = BigInt(args.amount0.toString()) |
133 | | - const amt1 = BigInt(args.amount1.toString()) |
134 | | - if (amt0 > 0n) totalFee0 += (amt0 * BigInt(p.fee)) / 1_000_000n |
135 | | - if (amt1 > 0n) totalFee1 += (amt1 * BigInt(p.fee)) / 1_000_000n |
136 | | - } |
137 | | - } catch (_) {} |
138 | | - |
139 | | - const feeValue0 = new BigNumber(totalFee0.toString()).div(`1e${t0.decimals}`).times(price0) |
140 | | - const feeValue1 = new BigNumber(totalFee1.toString()).div(`1e${t1.decimals}`).times(price1) |
141 | | - const feeUsd = feeValue0.plus(feeValue1) |
142 | | - |
143 | | - const aprBn = tvl.gt(0) ? feeUsd.div(tvl).times(36500) : new BigNumber(0) |
144 | | - const apy = aprBn.toNumber() |
145 | | - |
146 | | - const feeTier = Number(p.fee) / 1_000_000 |
147 | | - const feeUsdNum = isFinite(feeUsd.toNumber()) ? feeUsd.toNumber() : 0 |
148 | | - const volumeUsd1d = feeTier > 0 ? feeUsdNum / feeTier : 0 |
149 | | - |
150 | | - dataPools.push({ |
151 | | - pool: p.pool, |
| 49 | + return { |
| 50 | + pool: p.id, |
152 | 51 | chain, |
153 | 52 | project: 'koalaswap', |
154 | | - symbol: `${t0.symbol}-${t1.symbol}`, |
155 | | - poolMeta: `${Number(p.fee) / 1e4}%`, |
156 | | - tvlUsd: tvl.toNumber(), |
157 | | - apyBase: apy, |
158 | | - underlyingTokens: [p.token0, p.token1], |
159 | | - url: `${config.uiBase}/pools/${p.pool}`, |
| 53 | + symbol: `${p.token0.symbol}-${p.token1.symbol}`, |
| 54 | + poolMeta: `${Number(p.feeTier) / 1e4}%`, |
| 55 | + tvlUsd, |
| 56 | + apyBase, |
| 57 | + underlyingTokens: [p.token0.id, p.token1.id], |
| 58 | + url: `${config.uiBase}/pools/${p.id}`, |
160 | 59 | volumeUsd1d, |
161 | | - }) |
162 | | - } |
| 60 | + } |
| 61 | + }) |
163 | 62 |
|
164 | | - return dataPools |
| 63 | + return dataPools.filter((p) => utils.keepFinite(p)) |
165 | 64 | } |
166 | 65 |
|
167 | 66 | async function main() { |
168 | 67 | const data = await getPools() |
169 | | - return data.filter((p) => utils.keepFinite(p)) |
| 68 | + return data |
170 | 69 | } |
171 | 70 |
|
172 | 71 | module.exports = { |
|
0 commit comments