diff --git a/adapters/connext/src/utils/assets.ts b/adapters/connext/src/utils/assets.ts new file mode 100644 index 00000000..300b5664 --- /dev/null +++ b/adapters/connext/src/utils/assets.ts @@ -0,0 +1,84 @@ +import { createPublicClient, http, parseUnits } from "viem"; +import { linea } from "viem/chains"; +import { PoolInformation, getPoolInformationFromLpToken } from "./cartographer"; +import { LINEA_CHAIN_ID, CONNEXT_LINEA_ADDRESS } from "./subgraph"; +import { LpAccountBalanceHourly, RouterEventResponse } from "./types"; + +type CompositeBalanceHourly = LpAccountBalanceHourly & { + underlyingTokens: string[]; + underlyingBalances: string[]; +} + +const CONNEXT_ABI = [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "key", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "calculateRemoveSwapLiquidity", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] + +export const getCompositeBalances = async (amms: LpAccountBalanceHourly[]): Promise => { + // get lp token balances + const poolInfo = new Map(); + + // get pool info + await Promise.all(amms.map(async d => { + const poolId = d.token.id.toLowerCase(); + if (poolInfo.has(poolId)) { + return; + } + const pool = await getPoolInformationFromLpToken(d.token.id, LINEA_CHAIN_ID); + poolInfo.set(poolId, pool); + })); + + // get contract interface + const client = createPublicClient({ chain: linea, transport: http() }); + + // get composite balances for amms (underlying tokens and balances) + const balances = await Promise.all(amms.map(async ({ token, amount, block }) => { + const poolId = token.id.toLowerCase(); + const pool = poolInfo.get(poolId); + if (!pool) { + throw new Error(`Pool info not found for token: ${token.id}`); + } + // calculate the swap if you remove equal + const withdrawn = await client.readContract({ + address: CONNEXT_LINEA_ADDRESS, + functionName: "calculateRemoveSwapLiquidity", + args: [pool.key, parseUnits(amount, 18)], + abi: CONNEXT_ABI, + blockNumber: BigInt(block) + }) as [bigint, bigint]; + return withdrawn.map(w => w.toString()); + })); + + // return composite balance object + const ret = amms.map((d, idx) => { + const { pooledTokens } = poolInfo.get(d.token.id.toLowerCase())!; + return { + ...d, + underlyingTokens: pooledTokens, + underlyingBalances: balances[idx] as [string, string] + } + }) + return ret; +} diff --git a/adapters/connext/src/utils/cartographer.ts b/adapters/connext/src/utils/cartographer.ts index 1894b17f..b57a363e 100644 --- a/adapters/connext/src/utils/cartographer.ts +++ b/adapters/connext/src/utils/cartographer.ts @@ -1,4 +1,6 @@ import { chainIdToDomain, domainToChainId } from "@connext/nxtp-utils"; +import { BlockData, OutputDataSchemaRow, RouterEventResponse } from "./types"; +import { parseUnits } from "viem"; export type PoolInformation = { lpToken: string; @@ -34,4 +36,99 @@ export const getPoolInformationFromLpToken = async (lpToken: string, chainId: nu pooledTokenDecimals: pool_token_decimals, chainId: domainToChainId(+domain) } -}; \ No newline at end of file +}; + + +export const getRouterBalanceAtBlock = async (block: number, interval = 1000, account?: string) => { + let hasMore = true; + let offset = 0; + const balances = new Map() + + while (hasMore) { + const liquidityEvents = await getRouterLiquidityEvents(interval, block, account) + appendCartographerData(liquidityEvents, balances); + hasMore = liquidityEvents.length === interval; + offset += interval; + } + return [...balances.values()].flat(); +}; + +const appendCartographerData = (toAppend: RouterEventResponse[], existing: Map) => { + // get the latest record for each account. map should be keyed on router address, + // and store an array of locked router balances + toAppend.forEach((entry) => { + // no tally for account, set and continue + if (!existing.has(entry.router.toLowerCase())) { + existing.set(entry.router.toLowerCase(), [entry]); + return; + } + + // get the existing record for the router + const prev = existing.get(entry.router.toLowerCase())!; + // get the asset idx for this event + const idx = prev.findIndex((r) => r.asset.toLowerCase() === entry.asset.toLowerCase()); + if (idx < 0) { + // no record for this asset. append entry to existing list + existing.set(entry.router.toLowerCase(), [...prev, entry]); + return; + } + + // if the existing record is more recent, exit without updating + if (prev[idx].block_number >= entry.block_number) { + return; + } + prev[idx] = entry; + existing.set(entry.router.toLowerCase(), prev.filter((_, i) => idx !== i).concat([entry])); + }); +} + +export const getRouterLiquidityEvents = async ( + limit: number, + blockNumber: number, + router?: string +): Promise => { + const url = `${MAINNET_CARTOGRAPHER_URL}/router_liquidity_events?block_number=lte.${blockNumber}&limit=eq.${limit}${router ? `&router=eq.${router}` : ''}`; + const response = await fetch(url); + const data: RouterEventResponse[] = await response.json(); + return data; +} + +type AssetConfiguration = { + local: string; + adopted: string; + canonical_id: string; + canonical_domain: string; + domain: string; + key: string; + id: string; + decimal: number; + adopted_decimal: number +}; + +export const getAssets = async (): Promise => { + const url = `${MAINNET_CARTOGRAPHER_URL}/assets?domain=eq.1818848877`; + const response = await fetch(url); + return await response.json() as AssetConfiguration[]; +} + +export const formatRouterLiquidityEvents = async (block: BlockData, data: RouterEventResponse[]): Promise => { + // Get the asset information + const assets = await getAssets(); + + // Format the data + return data.map(d => { + const config = assets.find(a => a.local.toLowerCase() === d.asset.toLowerCase()); + const decimals = config ? config.decimal : 18; + const toParse = d.balance < 0 ? 0 : d.balance; + const balance = toParse.toString().includes('e') ? BigInt(toParse * 10 ** 18) : parseUnits(toParse.toString(), decimals) + return { + block_number: block.blockNumber, + timestamp: block.blockTimestamp, + user_address: d.router, + token_address: config ? config.adopted : d.asset.toLowerCase(), + token_balance: balance, + token_symbol: '', + usd_price: 0 + } + }) +} \ No newline at end of file diff --git a/adapters/connext/src/utils/getUserTvlByBlock.ts b/adapters/connext/src/utils/getUserTvlByBlock.ts index f3ba22b5..59739f54 100644 --- a/adapters/connext/src/utils/getUserTvlByBlock.ts +++ b/adapters/connext/src/utils/getUserTvlByBlock.ts @@ -1,23 +1,24 @@ -import { getBlock, getCompositeBalances, getLpAccountBalanceAtBlock } from "./subgraph"; +import { formatRouterLiquidityEvents, getRouterBalanceAtBlock } from "./cartographer"; +import { getLpAccountBalanceAtBlock } from "./subgraph"; import { BlockData, OutputDataSchemaRow } from "./types"; +import { getCompositeBalances } from "./assets"; export const getUserTVLByBlock = async (blocks: BlockData): Promise => { - const { blockNumber } = blocks + const { blockNumber, blockTimestamp } = blocks - const data = await getLpAccountBalanceAtBlock(blockNumber); + const amms = await getLpAccountBalanceAtBlock(blockNumber); - // get the composite balances - const composite = await getCompositeBalances(data); - // get block info - const { timestamp } = await getBlock(blockNumber); + // get the composite balances + const composite = await getCompositeBalances(amms); // format into output const results: OutputDataSchemaRow[] = []; + // format amm lps composite.forEach(({ account, underlyingBalances, underlyingTokens }) => { results.push(...underlyingBalances.map((b, i) => { const formatted: OutputDataSchemaRow = { - timestamp: +timestamp.toString(), + timestamp: blockTimestamp, block_number: blockNumber, user_address: account.id, token_address: underlyingTokens[i], @@ -29,6 +30,10 @@ export const getUserTVLByBlock = async (blocks: BlockData): Promise => { - // get lp token balances - const poolInfo = new Map(); - - // get pool info - await Promise.all(data.map(async d => { - const poolId = d.token.id.toLowerCase(); - if (poolInfo.has(poolId)) { - return; - } - const pool = await getPoolInformationFromLpToken(d.token.id, LINEA_CHAIN_ID); - poolInfo.set(poolId, pool); - })); - - // get contract interface - const client = createPublicClient({ chain: linea, transport: http() }); - - // get composite balances - const balances = await Promise.all(data.map(async ({ token, amount, block }) => { - const poolId = token.id.toLowerCase(); - const pool = poolInfo.get(poolId); - if (!pool) { - throw new Error(`Pool info not found for token: ${token.id}`); - } - // calculate the swap if you remove equal - const withdrawn = await client.readContract({ - address: CONNEXT_LINEA_ADDRESS, - functionName: "calculateRemoveSwapLiquidity", - args: [pool.key, parseUnits(amount, 18)], - abi: CONNEXT_ABI, - blockNumber: BigInt(block) - }) as [bigint, bigint]; - return withdrawn.map(w => w.toString()); - })); - - // return composite balance object - return data.map((d, idx) => { - const { pooledTokens } = poolInfo.get(d.token.id.toLowerCase())!; - return { - ...d, - underlyingTokens: pooledTokens, - underlyingBalances: balances[idx] as [string, string] - } - }) -} - const appendSubgraphData = (data: LpAccountBalanceHourly[], existing: Map) => { // looking for latest record of account balance data.forEach(d => { diff --git a/adapters/connext/src/utils/types.ts b/adapters/connext/src/utils/types.ts index 925ea22c..653fae35 100644 --- a/adapters/connext/src/utils/types.ts +++ b/adapters/connext/src/utils/types.ts @@ -32,4 +32,19 @@ export type LpAccountBalanceHourly = { // Subgraph query result export type SubgraphResult = { lpAccountBalanceHourlies: LpAccountBalanceHourly[]; -} \ No newline at end of file +} + +// Router liquidity event response +export type RouterEventResponse = { + id: string; + domain: string; + router: string; + event: 'Add' | 'Remove'; + asset: string; + amount: number; + balance: number; + block_number: number; + transaction_hash: string; + timestamp: number; + nonce: number; +}