wormhole-explorer/onchain_data/src/getEvmCustody.ts

376 lines
11 KiB
TypeScript

import { formatUnits } from "ethers/lib/utils";
import { ethers } from "ethers";
import { getTokenPricesCGID, getTokenPricesGET } from "./getCoinGeckoPrices";
import {
CHAIN_ID_OASIS,
isEVMChain,
CHAIN_ID_KARURA,
CHAIN_ID_ACALA,
CHAIN_ID_CELO,
getEmitterAddressEth,
} from "@certusone/wormhole-sdk";
import { abi as Erc20Abi } from "./abi/erc20.json";
import axios from "axios";
import {
DISALLOWLISTED_ADDRESSES,
CHAIN_INFO_MAP,
newProvider,
sleepFor,
} from "./utils";
// current allowlist used for stats/govenor
import allowList = require("./allowList.json");
require("dotenv").config();
function getAllowList(chainId) {
if (Object.keys(allowList).includes(chainId.toString())) {
return allowList[chainId];
} else {
return [];
}
}
function calcTokenQty(tokenInfo) {
return Number(formatUnits(tokenInfo.balance, tokenInfo.decimals));
}
async function getBridgeBalanceScanner(chainInfo) {
const chainId = chainInfo.chain_id;
const bridgeAddress = chainInfo.token_bridge_address;
const url = chainInfo.urlStem;
// Get native token balances locked in contract
const balance_apireqstring = `${url}/api?module=account&action=balance&address=${bridgeAddress}`;
const balance_resp = await axios.get(balance_apireqstring);
var tokenAccounts = [];
if (balance_resp.status == 200) {
const data = balance_resp["data"];
if (data.hasOwnProperty("result")) {
const balance = data["result"];
var nativeInfo = {};
var symbol = "";
if (chainId == 7) {
symbol = "ROSE";
} else if (chainId == 11) {
symbol = "KAR";
} else if (chainId == 12) {
symbol: "ACA";
} else if (chainId == 14) {
symbol: "CELO";
}
nativeInfo = {
decimals: 18,
name: symbol,
tokenAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2",
balance: balance,
symbol: symbol,
};
tokenAccounts.push(nativeInfo);
} else {
console.log("object has no native balance data");
}
} else {
console.log(balance_resp.status);
}
// Get locked token list from scan site
const apiReqString = `${url}/api?module=account&action=tokenlist&address=${bridgeAddress}`;
console.log(apiReqString);
const response = await axios.get(apiReqString);
if (response.status == 200) {
const data = response["data"];
if (data.hasOwnProperty("result")) {
const tokenData = data["result"];
tokenAccounts = [
...tokenAccounts,
...tokenData.map((x) => ({
decimals: x.decimals,
name: x.name,
tokenAddress: x.contractAddress,
balance: x.balance,
symbol: x.symbol,
})),
];
return tokenAccounts;
} else {
console.log("object has no data");
return [];
}
} else {
console.log(response.status);
return [];
}
}
async function getBridgeBalanceCovalent(chainInfo) {
/* Get token list (also has bridge balance and prices, but could be stale, so will query onchain)*/
const covalentChainId: string = chainInfo.covalentChain;
const bridgeAddress: string = chainInfo.token_bridge_address;
const covalentApiKey = process.env.REACT_APP_COVALENT_API_KEY;
const apiReqString = `https://api.covalenthq.com/v1/${covalentChainId}/address/${bridgeAddress}/balances_v2/?quote-currency=USD&format=JSON&nft=true&no-nft-fetch=false&key=${covalentApiKey}`;
console.log(apiReqString);
const response = await axios.get(apiReqString, {
headers: { "User-Agent": "Mozilla/5.0" },
});
if (response.status == 200) {
const data = response["data"];
if (data.hasOwnProperty("data")) {
const tokenData = data["data"];
// tokenData["items"].forEach((x) => console.log(x));
const tokenAccounts = tokenData["items"]
.filter((item) => item.type !== "nft")
.map((x) => ({
decimals: x.contract_decimals,
name: x.contract_name,
tokenAddress: x.contract_address,
balance: x.balance,
price: x.quote_rate,
}));
return tokenAccounts;
} else {
console.log("object has no data");
return [];
}
} else {
console.log(response.status);
return [];
}
}
//for evm chains
async function getTokenContract(
address: string,
provider:
| ethers.providers.JsonRpcProvider
| ethers.providers.JsonRpcBatchProvider
) {
const contract = new ethers.Contract(address, Erc20Abi, provider);
return contract;
}
async function getBridgeBalanceOnChain(chainInfo, tokenList: any[]) {
const bridgeAddress = chainInfo.token_bridge_address;
let provider = newProvider(
chainInfo.endpoint_url,
true
) as ethers.providers.JsonRpcBatchProvider;
var tokenAccounts = [];
let i = 0;
let chunksize = 100;
while (i < tokenList.length) {
const tokenContracts = await Promise.all(
tokenList
.slice(i, i + chunksize)
.map((token) => getTokenContract(token.tokenAddress, provider))
);
const tokenInfos = await Promise.all(
tokenContracts.map((tokenContract) =>
Promise.all([
tokenContract.address.toLowerCase(),
tokenContract.name(),
tokenContract.decimals(),
tokenContract.symbol(),
tokenContract.balanceOf(bridgeAddress),
])
)
);
tokenInfos.forEach((tokenInfo) => {
tokenAccounts.push({
tokenAddress: tokenInfo[0],
name: tokenInfo[1],
decimals: tokenInfo[2],
symbol: tokenInfo[3],
balance: tokenInfo[4],
});
});
i += chunksize;
}
return tokenAccounts;
}
export async function getEvmTokenAccounts(chainInfo, useAllowList) {
const chainId = chainInfo.chain_id;
console.log(chainId);
if (!isEVMChain(chainId)) {
console.log(`error. ${chainId} is not evm chain`);
return [];
}
var tokenAccounts = [];
if (useAllowList) {
const allowList = getAllowList(chainId);
var tokenList = [];
Object.keys(allowList).forEach((address) => {
tokenList.push({
tokenAddress: address,
});
});
// console.log(tokenList);
tokenAccounts = await getBridgeBalanceOnChain(
chainInfo,
tokenList.filter(
(token) =>
token.tokenAddress.toLowerCase() !=
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" // filter out native token
)
);
// console.log("tokenAccounts", tokenAccounts);
} else {
if (
chainId == CHAIN_ID_OASIS ||
chainId == CHAIN_ID_KARURA ||
chainId == CHAIN_ID_ACALA ||
chainId == CHAIN_ID_CELO
) {
tokenAccounts = await getBridgeBalanceScanner(chainInfo);
} else {
// use convalent to get token addresses
const tokenList = await getBridgeBalanceCovalent(chainInfo);
// console.log(tokenList.filter((token) => console.log(token)));
tokenAccounts = await getBridgeBalanceOnChain(
chainInfo,
tokenList.filter(
(token) =>
token.tokenAddress.toLowerCase() !=
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" // native token
)
);
// console.log(tokenAccounts);
}
}
return tokenAccounts;
}
async function getTokenValues(chainInfo, tokenInfos: any[], useAllowList) {
console.log("allowlist?", useAllowList);
try {
const custody = tokenInfos.map((tokenInfo) => ({
...tokenInfo,
qty: calcTokenQty(tokenInfo),
}));
const custodyFiltered = custody.filter((c) => c.qty > 0);
var tokenPrices = {};
var prices = [];
if (useAllowList) {
// use coingecko ids from allowlist
const allowList = getAllowList(chainInfo.chain_id);
const cgids: string[] = Object.values(allowList);
// input array of cgids, returns json with cgid:price
prices = await getTokenPricesCGID(cgids);
if (prices === undefined) {
console.log(`could not find ids for ${chainInfo.chain_id}`);
return [];
}
for (const [key, value] of Object.entries(prices)) {
if (!value.hasOwnProperty("usd")) {
prices[key] = { usd: 0 };
}
}
// have to map cgid: price to tokenAddress: price
for (const [key, value] of Object.entries(allowList)) {
for (const [key1, value1] of Object.entries(prices)) {
if (key1 === value) {
tokenPrices[key] = prices[key1];
}
}
}
// console.log(tokenPrices);
} else {
// use tokenAddresses from tokenInfos/custody
let j = 0;
let chunk_size = 100;
while (j < custodyFiltered.length) {
prices = await getTokenPricesGET(
chainInfo.chain_id,
chainInfo.platform,
custodyFiltered.slice(j, j + chunk_size).map((x) => x.tokenAddress)
);
for (const [key, value] of Object.entries(prices)) {
if (!value.hasOwnProperty("usd")) {
prices[key] = { usd: 0 };
}
}
tokenPrices = { ...tokenPrices, ...prices };
j += chunk_size;
}
}
// filter list by those with coin gecko prices
const filteredBalances = custodyFiltered.filter((x) =>
Object.keys(tokenPrices).includes(x.tokenAddress)
);
// calculate usd balances. add price and usd balance to tokenInfos
const balancesUSD = filteredBalances.map((tokenInfo) => ({
...tokenInfo,
tokenPrice: tokenPrices[tokenInfo.tokenAddress]["usd"],
tokenBalanceUSD:
tokenInfo.qty * tokenPrices[tokenInfo.tokenAddress]["usd"],
}));
// filter out disallowlist addresses
const balancesUSDFiltered = balancesUSD.filter(
(x) => !DISALLOWLISTED_ADDRESSES.includes(x.tokenAddress)
);
const sorted = balancesUSDFiltered.sort((a, b) =>
a.tokenBalanceUSD < b.tokenBalanceUSD ? 1 : -1
);
return sorted;
} catch (e) {
console.log(e);
}
return [];
}
export async function getEvmCustody(chainInfo, useAllowList = true) {
const tokenAccounts = await getEvmTokenAccounts(chainInfo, useAllowList);
console.log(
`Num of ${chainInfo.platform} token accounts=${tokenAccounts.length}`
);
const custody = await getTokenValues(chainInfo, tokenAccounts, useAllowList);
console.log(
`Num of filtered ${chainInfo.platform} token accounts=${custody.length}`
);
return custody;
}
export async function grabEvmCustodyData(chain, useAllowList) {
const chainInfo = CHAIN_INFO_MAP[chain];
var balances = [];
try {
balances = await getEvmCustody(chainInfo, useAllowList);
} catch (e) {
console.log(`could not grab ${chainInfo.name} data`);
}
// await updateTable(chainInfo, balances);
const chainInfo_ = {
...chainInfo,
emitter_address: getEmitterAddressEth(chainInfo.token_bridge_address),
balances: balances,
};
return chainInfo_;
}
// const chain = process.env.chain;
// const useAllowListstr = process.env.chain || "false";
// (async () => {
// const chainInfo = CHAIN_INFO_MAP[chain];
// const useAllowList = true ? useAllowListstr === "true" : false;
// const balances = await getEvmCustody(chainInfo, useAllowList);
// console.log(balances);
// await updateTable(chainInfo, balances);
// })();