Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

connext : bridge : routers #167

Merged
merged 1 commit into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions adapters/connext/src/utils/assets.ts
Original file line number Diff line number Diff line change
@@ -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<CompositeBalanceHourly[]> => {
// get lp token balances
const poolInfo = new Map<string, PoolInformation>();

// 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;
}
99 changes: 98 additions & 1 deletion adapters/connext/src/utils/cartographer.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,4 +36,99 @@ export const getPoolInformationFromLpToken = async (lpToken: string, chainId: nu
pooledTokenDecimals: pool_token_decimals,
chainId: domainToChainId(+domain)
}
};
};


export const getRouterBalanceAtBlock = async (block: number, interval = 1000, account?: string) => {
let hasMore = true;
let offset = 0;
const balances = new Map<string, RouterEventResponse[]>()

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<string, RouterEventResponse[]>) => {
// 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<RouterEventResponse[]> => {
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<AssetConfiguration[]> => {
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<OutputDataSchemaRow[]> => {
// 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
}
})
}
23 changes: 14 additions & 9 deletions adapters/connext/src/utils/getUserTvlByBlock.ts
Original file line number Diff line number Diff line change
@@ -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<OutputDataSchemaRow[]> => {
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],
Expand All @@ -29,6 +30,10 @@ export const getUserTVLByBlock = async (blocks: BlockData): Promise<OutputDataSc
}));
})

return results;
// get the router balances
const routers = await getRouterBalanceAtBlock(blockNumber);
const formatted = await formatRouterLiquidityEvents(blocks, routers);

return results.concat(formatted);
};

77 changes: 0 additions & 77 deletions adapters/connext/src/utils/subgraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,6 @@ export const CONNEXT_SUBGRAPH_QUERY_URL = "https://api.goldsky.com/api/public/pr
export const LINEA_CHAIN_ID = 59144;
export const CONNEXT_LINEA_ADDRESS = "0xa05eF29e9aC8C75c530c2795Fa6A800e188dE0a9";

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"
}
]

const LP_HOURLY_QUERY_BY_BLOCK = (
first: number,
Expand Down Expand Up @@ -138,57 +112,6 @@ export const getLpAccountBalanceAtTimestamp = async (timestamp: number, interval
return [...balances.values()].flat();
}

type CompositeBalanceHourly = LpAccountBalanceHourly & {
underlyingTokens: [string, string];
underlyingBalances: [string, string];
}

export const getCompositeBalances = async (data: LpAccountBalanceHourly[]): Promise<CompositeBalanceHourly[]> => {
// get lp token balances
const poolInfo = new Map<string, PoolInformation>();

// 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<string, LpAccountBalanceHourly[]>) => {
// looking for latest record of account balance
data.forEach(d => {
Expand Down
17 changes: 16 additions & 1 deletion adapters/connext/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,19 @@ export type LpAccountBalanceHourly = {
// Subgraph query result
export type SubgraphResult = {
lpAccountBalanceHourlies: LpAccountBalanceHourly[];
}
}

// 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;
}
Loading