From ca0e6686a766111876ff498106b0659af6068518 Mon Sep 17 00:00:00 2001 From: Sneed Infinex Date: Wed, 17 Jul 2024 15:35:18 +1000 Subject: [PATCH] feat: add infinex adapter --- projects/infinex/api.js | 20 ++++++ projects/infinex/index.js | 39 ++++++++++ projects/infinex/solana.js | 141 +++++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 projects/infinex/api.js create mode 100644 projects/infinex/index.js create mode 100644 projects/infinex/solana.js diff --git a/projects/infinex/api.js b/projects/infinex/api.js new file mode 100644 index 00000000000..2f7baecaff6 --- /dev/null +++ b/projects/infinex/api.js @@ -0,0 +1,20 @@ +const http = require("../helper/http"); + +const INFINEX_API_BASE_URL = "https://api.app.infinex.xyz" + +async function getAccountAddresses() { + return http.get(`${INFINEX_API_BASE_URL}/public/accounts`); +} + +async function getChainAssets(chain) { + const assetsJson = await http.get(`${INFINEX_API_BASE_URL}/public/chainAssets`); + + return assetsJson + .filter(i => i.chain === chain && i.address !== 'native') + .map(v => v.address); +} + +module.exports = { + getAccountAddresses, + getChainAssets +} diff --git a/projects/infinex/index.js b/projects/infinex/index.js new file mode 100644 index 00000000000..2752145c9df --- /dev/null +++ b/projects/infinex/index.js @@ -0,0 +1,39 @@ +const { sumTokens2: sumTokens2Evm } = require('../helper/unwrapLPs') +const { sumTokens2Batched: sumTokens2Solana } = require('./solana'); +const { getAccountAddresses, getChainAssets } = require('./api'); + +async function tvlEvm(api) { + const tokens = await getChainAssets(api.chain); + const owners = (await getAccountAddresses()).map(address => address.evm).filter(address => !!address); + + return sumTokens2Evm({ owners, tokens, api, sumChunkSize: 30000 }); +} + +async function tvlSolana(api) { + const tokens = await getChainAssets(api.chain); + const owners = (await getAccountAddresses()).map(address => address.solana).filter(address => !!address); + + return sumTokens2Solana({ owners, tokens }); +} + +module.exports = { + methodology: `Sums native + ERC-20 + SPL token balances of all Infinex accounts`, + base: { + tvl: tvlEvm + }, + arbitrum: { + tvl: tvlEvm + }, + optimism: { + tvl: tvlEvm + }, + polygon: { + tvl: tvlEvm + }, + ethereum: { + tvl: tvlEvm + }, + solana: { + tvl: tvlSolana + }, +} diff --git a/projects/infinex/solana.js b/projects/infinex/solana.js new file mode 100644 index 00000000000..db4e37e8d20 --- /dev/null +++ b/projects/infinex/solana.js @@ -0,0 +1,141 @@ +const ADDRESSES = require('../helper/coreAssets.json') +const axios = require("axios"); +const { sleep } = require('../helper/utils'); +const { blacklistedTokens_default, endpoint, transformBalances } = require('../helper/solana'); +const { log } = require('@defillama/sdk'); +const { sliceIntoChunks } = require('@defillama/sdk/build/util'); +const { PublicKey } = require('@solana/web3.js'); + +// Address of the SPL Token program +const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); +const TOKEN_PROGRAM_ID_BUFFER = TOKEN_PROGRAM_ID.toBuffer(); + +// Address of the SPL Associated Token Account program +const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); + +async function sumTokens2Batched( + { tokens, owners } +) { + log("Calculating solana token accounts...") + const tokenAccountsWithMints = tokens + .filter((token) => !blacklistedTokens_default.includes(token)) + // this is synchronous and blocks propotional to the number of accounts + .flatMap(token => owners.map(owner => [token, getAssociatedTokenAccountAddress(owner, token)])) + const tokenAccounts = tokenAccountsWithMints.map(([_, tokenAccount]) => tokenAccount); + + const solBalances = await getSolBalances(owners); + const splBalances = await getTokenAccountBalances(tokenAccounts); + + const solTokenBalances = solBalances.flatMap(balance => balance ? [[ADDRESSES.solana.SOL, balance]] : []); + const splTokenBalances = splBalances.flatMap((balance, idx) => balance ? [[tokenAccountsWithMints[idx][0], balance]] : []) + + const tokenBalances = [...solTokenBalances, ...splTokenBalances].reduce((acc, [mint, value]) => { + return (value ?? 0n) > 0n + ? { + ...acc, + [mint]: (acc[mint] ?? 0n) + value + } + : acc + }, {}); + + return transformBalances({ tokenBalances }); +} + +async function getTokenAccountBalances( + tokenAccountAddresses +) { + const data = await getMultipleAccountsBatched( + tokenAccountAddresses, + // get _just_ the account balance + { length: 64, offset: 64 } + ); + + return data + .map(value => value ? BigInt(value.data.parsed.info.tokenAmount.amount) : null) +} + +async function getSolBalances( + owners +) { + const data = await getMultipleAccountsBatched( + owners, + ); + + return data.map(value => value ? BigInt(value.lamports) : null); +} + +function getAssociatedTokenAccountAddress( + owner, + token +) { + return PublicKey.findProgramAddressSync( + [ + new PublicKey(owner).toBuffer(), + TOKEN_PROGRAM_ID_BUFFER, + new PublicKey(token).toBuffer(), + ], + ASSOCIATED_TOKEN_PROGRAM_ID + )[0].toBase58(); +} + +async function getMultipleAccountsBatched( + addresses, + dataSlice +) { + // this lets us query for 100 * 50 * 1 (5000k) accounts at a time + const rpcMethodChunks = sliceIntoChunks(addresses, 100); + const httpRequestChunks = sliceIntoChunks(rpcMethodChunks, 25); + const promiseChunks = sliceIntoChunks(httpRequestChunks, 1); + + const data = []; + for (const promiseChunk of promiseChunks) { + const result = ( + await Promise.all( + promiseChunk.map(httpRequestChunk => + solanaRpcRetry({ + body: httpRequestChunk.map(rpcMethodChunk => ({ + jsonrpc: "2.0", + id: 1, + method: "getMultipleAccounts", + params: [rpcMethodChunk, { + dataSlice, + encoding: "jsonParsed" + }], + })) + }) + ) + )).flat(); + + result + .forEach(item => data.push(...item.result.value)) + + await sleep(500); + } + + return data; +} + +async function solanaRpcRetry( + { + body, + maxRetries = 7, + retryCount = 0, + } +) { + try { + const response = await axios.post(endpoint, body); + return response.data; + } catch (err) { + if (err?.response?.status === 429 && retryCount < maxRetries) { + log(`Hit Solana RPC rate limit. Retrying...`); + await sleep((2 ** retryCount) * 1000); + return solanaRpcRetry({ body, maxRetries, retryCount: retryCount + 1 }); + } + + throw err; + } +} + +module.exports = { + sumTokens2Batched +}