Added onchain_data/custody support (#5)

* Added oncahin_data/custody support

* Delete erc20.json

duplicate/unnecessary file

* Delete allowList.json

duplicate file

* Update getCoinGeckoPrices.ts
This commit is contained in:
ckeun 2022-10-28 10:24:25 -05:00 committed by GitHub
parent a8b44bdbfc
commit cc01fb6456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 12445 additions and 17 deletions

2
onchain_data/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
lib

20
onchain_data/README.md Normal file
View File

@ -0,0 +1,20 @@
# Onchain data for custody monitoring
For each connected wormhole chain, query the custody contract and get the updated locked value for each token.
There are two modes:
(1) query a known list of tracked tokens aka allow list
(2) query either a scan site (if available) or covalent (also if available) to grab list of known tokens locked in the custody contract
Set env variable allowlist=true if using allow list found at: onchain_data/data/allowList.json
taken from: https://github.com/wormhole-foundation/wormhole/blob/dev.v2/event_database/cloud_functions/token-allowlist-mainnet.json
Has the added benefit of also including the coin gecko ids in order to mark positions to USD
```
MONGODB_URI=mongodb://root:example@localhost:27017/ allowlist=true node lib/getCustodyData.js
```
Currently Near, Algorand, Xpla, & Aptos are not supported. Feel free to add support ;)
The aggregated custody data gets pushed to the mongodb database - "onchain_data" into the table - "custody"
There are api endpoints that the server provides which powers the wormscan website.

9540
onchain_data/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
onchain_data/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"dependencies": {
"@certusone/wormhole-sdk": "^0.7.1",
"@metaplex/js": "^4.12.0",
"@terra-money/terra.js": "^3.1.3",
"coingecko-api": "^1.0.10",
"mongodb": "^4.10.0",
"ts-node": "^10.9.1"
},
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^4.8.4"
}
}

View File

@ -0,0 +1,224 @@
{
"abi": [
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]
}

View File

@ -0,0 +1,173 @@
{
"1": {
"ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx": "star-atlas",
"NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk": "blockasset",
"AkhdZGVbJXPuQZ53u2LrimCjkRP6ZyxG1SoM85T98eE1": "starbots",
"4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R": "raydium",
"4Te4KJgjtnZe4aE2zne8G4NPfrPjCwDmaiEx9rKnyDVZ": "solclout",
"So11111111111111111111111111111111111111112": "wrapped-solana",
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": "usd-coin",
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": "tether",
"7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj": "lido-staked-sol"
},
"2": {
"0x111111111117dc0aa78b770fa6a738034120c302": "1inch",
"0x009178997aff09a67d4caccfeb897fb79d036214": "1sol",
"0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9": "aave",
"0x27702a26126e0b3702af63ee09ac4d1a084ef628": "aleph",
"0xe0cca86b254005889ac3a81e737f56a14f4a38f5": "alta-finance",
"0x9b83f827928abdf18cf1f7e67053572b9bceff3a": "artem",
"0x18aaa7115705e8be94bffebde57af9bfc265b998": "audius",
"0xbb0e17ef65f82ab018d8edd776e8dd940327b28b": "axie-infinity",
"0x0d8775f648430679a709e98d2b0cb6250d2887ef": "basic-attention-token",
"0xf17e65822b568b3903685a7c9f496cf7656cc6c2": "biconomy",
"0xef19f4e48830093ce5bc8b3ff7f903a0ae3e9fa1": "botxcoin",
"0xbba39fd2935d5769116ce38d46a71bde9cf03099": "choise",
"0xd49efa7bc0d339d74f487959c573d518ba3f8437": "shield-finance",
"0xc00e94cb662c3520282e6f5717214004a7f26888": "compound-governance-token",
"0x2ba592f78db6436527729929aaf6c908497cb200": "cream-2",
"0x6b175474e89094c44da98b954eedeac495271d0f": "dai",
"0x92d6c1e31e14520e676a687f0a93788b716beff5": "dydx",
"0x4da34f8264cb33a5c9f17081b9ef5ff6091116f4": "elyfi",
"0xfd09911130e6930bf87f2b0554c44f400bd80d3e": "ethichub",
"0x853d955acef822db058eb8505911ed77f175b99e": "frax",
"0xf8c3527cc04340b208c854e985240c02f7b7793f": "frontier-token",
"0x50d1c9771902476076ecfc8b2a83ad6b9355a4c9": "ftx-token",
"0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0": "frax-share",
"0xc944e90c64b2c07662a292be6244bdf05cda44a7": "the-graph",
"0x4674672bcddda2ea5300f5207e1158185c944bc0": "gem-exchange-and-trading",
"0x0316eb71485b0ab14103307bf65a021042c6d380": "huobi-btc",
"0x4bd70556ae3f8a6ec6c4080a0c327b24325438f3": "hxro",
"0xe28b3b32b6c345a34ff64674606124dd5aceca30": "injective-protocol",
"0x8a9c67fee641579deba04928c4bc45f66e26343a": "jarvis-reward-token",
"0x85eee30c52b0b379b046fb0f85f4f3dc3009afec": "keep-network",
"0x5a98fcbea516cf06857215779fd812ca3bef1b32": "lido-dao",
"0x514910771af9ca656af840dff83e8264ecf986ca": "chainlink",
"0x0f5d2fb29fb7d3cfee444a200298f468908cc942": "decentraland",
"0x08d967bb0134f2d07f7cfb6e246680c53927dd30": "math",
"0xe831f96a7a1dce1aa2eb760b1e296c6a74caa9d5": "nexum",
"0xdfdb7f72c1f195c5951a234e8db9806eb0635346": "feisty-doge-nft",
"0x727f064a78dc734d33eec18d5370aef32ffd46e4": "orion-money",
"0x45804880de22913dafe09f4980848ece6ecbaf78": "pax-gold",
"0x65e6b60ea01668634d68d0513fe814679f925bad": "pixelverse",
"0xf1f955016ecbcd7321c7266bccfb96c68ea5e49b": "rally-2",
"0x3845badade8e6dff049820680d1f14bd3903a5d0": "the-sandbox",
"0x30d20208d987713f46dfd34ef128bb16c404d10f": "stader",
"0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce": "shiba-inu",
"0x5ab6a4f46ce182356b6fa2661ed8ebcafce995ad": "sportium",
"0x476c5e26a75bd202a9683ffd34359c0cc15be0ff": "serum",
"0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": "sushi",
"0x8ce9137d39326ad0cd6491fb5cc0cba0e089b6a9": "swipe",
"0x2e95cea14dd384429eb3c4331b776c4cfbb6fcd9": "throne",
"0x05d3606d5c81eb9b7b18530995ec9b29da05faba": "tomoe",
"0x2c537e5624e4af88a7ae4060c022609376c8d0eb": "bilira",
"0x8564653879a18c560e7c0ea0e084c516c62f5653": "upbots",
"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984": "uniswap",
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "usd-coin",
"0xdac17f958d2ee523a2206206994597c13d831ec7": "tether",
"0x0c572544a4ee47904d54aaa6a970af96b6f00e1b": "wasder",
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": "wrapped-bitcoin",
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "ethereum",
"0x72b886d09c117654ab7da13a14d603001de0b777": "xdefi",
"0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e": "yearn-finance",
"0x1a7e4e63778b4f12a199c062f3efdd288afcbce8": "ageur",
"0x707f9118e33a9b8998bea41dd0d46f38bb963fc8": "ethereum",
"0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0": "wrapped-steth",
"0xa2cd3d43c775978a96bdbf12d733d5a1ed94fb18": "chain-2"
},
"3": {
"terra193c42lfwmlkasvcw22l9qqzc5q2dx208tkd7wl": "bitlocus",
"uluna": "terra-luna",
"terra13awdgcx40tz5uygkgm79dytez3x87rpg4uhnvu": "playnity",
"uusd": "terrausd",
"terra1hzh9vpxhsk8253se0vv5jj6etdvxu3nv8z07zu": "anchorust"
},
"4": {
"0x7e46d5eb5b7ca573b367275fee94af1945f5b636": "abitshadow-token",
"0xe9e7cea3dedca5984780bafc599bd69add087d56": "binance-usd",
"0x8ebc361536094fd5b4ffb8521e31900614c9f55d": "darcmatter-coin",
"0x2170ed0880ac9a755fd29b2688956bd959f933f8": "weth",
"0x3019bf2a2ef8040c242c9a4c5c4bd4c81678b2a1": "stepn",
"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d": "usd-coin",
"0x55d398326f99059ff775485246999027b3197955": "tether",
"0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c": "wbnb",
"0xfafd4cb703b25cb22f43d017e7e0d75febc26743": "weyu",
"0xfa40d8fc324bcdd6bbae0e086de886c571c225d4": "wizardia"
},
"5": {
"0x9c891326fd8b1a713974f73bb604677e1e63396d": "islamicoin",
"0x2791bca1f2de4661ed88a30c99a7a9449aa84174": "usd-coin",
"0xc2132d05d31c914a87c6611c10748aeb04b58e8f": "tether",
"0x7ceb23fd6bc0add59e62ac25578270cff1b9f619": "weth",
"0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270": "matic-network"
},
"6": {
"0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e": "usd-coin",
"0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664": "usd-coin",
"0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7": "tether",
"0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7": "avalanche-2",
"0x2b2c81e08f1af8835a78bb2a90ae924ace0ea4be": "benqi-liquid-staked-avax"
},
"7": {
"0x94fbffe5698db6f54d6ca524dbe673a7729014be": "usd-coin",
"0x366ef31c8dc715cbeff5fa54ad106dc9c25c6153": "tether-usd-wormhole-from-bsc",
"0x3223f17957ba502cbe71401d55a0db26e5f7c68f": "ethereum-wormhole",
"0x21c718c22d52d0f3a789b752d4c2fd5908a8a733": "oasis-network"
},
"8": {
"0": "algorand",
"31566704": "usd-coin",
"312769": "tether"
},
"9": {
"0x8BEc47865aDe3B172A928df8f990Bc7f2A3b9f79": "aurora",
"0xE4B9e004389d91e4134a28F19BD833cBA1d994B6": "frax",
"0xb12bfca5a55806aaf64e99521918a4bf0fc40802": "usd-coin",
"0x4988a896b1227218e4a686fde5eabdcabd91571f": "tether",
"0x5183e1b1091804bc2602586919e6880ac1cf2896": "usn",
"0xc9bdeed33cd01541e1eed10f90519d2c06fe3feb": "weth",
"0xC9BdeEd33CD01541e1eeD10f90519d2C06Fe3feB": "ethereum",
"0xc4bdd27c33ec7daa6fcfd8532ddb524bf4038096": "wrapped-terra"
},
"10": {
"0x321162cd933e2be498cd2267a90534a804051b11": "wrapped-bitcoin",
"0x74b23882a30290451a17c44f4f05243b6b58c76d": "weth",
"0x260b3e40c714ce8196465ec824cd8bb915081812": "iron-bsc",
"0x04068da6c83afcfa0e13ba15a6696662335d5b75": "usd-coin",
"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83": "wrapped-fantom"
},
"11": {
"0x0000000000000000000100000000000000000080": "karura",
"0x0000000000000000000100000000000000000082": "kusama",
"0x0000000000000000000500000000000000000007": "tether",
"0x0000000000000000000100000000000000000081": "acala-dollar"
},
"12": {
"0x0000000000000000000100000000000000000000": "acala",
"0x0000000000000000000100000000000000000002": "polkadot",
"0x0000000000000000000100000000000000000001": "acala-dollar"
},
"13": {
"0x5c74070fdea071359b86082bd9f9b3deaafbe32b": "dai",
"0x5fff3a6c16c2208103f318f4713d4d90601a7313": "kleva",
"0x5096db80b21ef45230c9e423c373f1fc9c0198dd": "wemix-token",
"0xe4f05a66ec68b54a58b17c22107b02e0232cc817": "klay-token",
"0xcee8faf64bb97a73bb51e115aa89c17ffa8dd167": "tether"
},
"14": {
"0x471ece3750da237f93b8e339c536989b8978a438": "celo",
"0x46c9757c5497c5b1f2eb73ae79b6b67d119b0b58": "impactmarket",
"0xd8763cba276a3738e6de85b4b3bf5fded6d6ca73": "celo-euro",
"0x765de816845861e75a25fca122bb6898b8b1282a": "celo-dollar"
},
"15": {
"near": "near",
"token.sweat": "sweatcoin"
},
"16": {
"0xacc15dc74880c9944775448304b263d191c6077f": "moonbeam"
},
"18": {
"uluna": "terra-luna-2"
}
}

View File

@ -0,0 +1,133 @@
import axios from "axios";
import { COIN_GECKO_EXCEPTIONS } from "./utils";
const CoinGecko = require("coingecko-api");
function sleep(milliseconds) {
const date = Date.now();
let currentDate = 0;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
//2. Initiate the CoinGecko API Client
const CoinGeckoClient = new CoinGecko(); //use justin's VIP coin gecko api key?
async function getCoinsList() {
let data = await CoinGeckoClient.coins.list();
//console.log(data)
return;
}
export async function getTokenPrice(tokenAddress) {
let tokenContractInfo = await CoinGeckoClient.coins.fetchCoinContractInfo(
tokenAddress
);
let data = tokenContractInfo["data"];
var price = 0;
try {
price = data["market_data"]["current_price"].usd;
} catch (e) {
console.log("could not find price for address=", tokenAddress);
}
return price;
}
export async function getTokenPrices(tokenAddresses) {
//only for ethereum chain
let tokenContractInfos;
try {
tokenContractInfos = await CoinGeckoClient.simple.fetchTokenPrice({
contract_addresses: tokenAddresses,
vs_currencies: "usd",
});
} catch (e) {
console.log(e);
console.log("could not find prices for addresses");
}
return tokenContractInfos["data"];
}
function getKeyByValue(object, value) {
return Object.keys(object).find((key) => object[key] === value);
}
export async function getCoinGeckoMap() {
// pull coin gecko mapping of token address to coingecko id
var coinMap;
const map_query = `https://api.coingecko.com/api/v3/coins/list?include_platform=true`;
try {
const req = await axios.get(map_query);
coinMap = req.data;
} catch (e) {
console.log(e);
console.log("could not find prices for addresses");
}
var coinMapTransformed: any[] = [];
for (let i = 0; i < coinMap.length; i++) {
const coin = coinMap[i];
const platforms = coin?.platforms;
Object.entries(platforms).forEach((entry) => {
const [platform, contractAddress] = entry;
if (contractAddress != "" && contractAddress != null) {
coinMapTransformed.push({
contractAddress: contractAddress,
coinGeckoId: coin.id,
});
}
});
}
return coinMapTransformed;
}
export async function getTokenPricesGET(
chainId: number,
platform: String,
tokenAddresses: String[]
) {
let data;
const addresses = tokenAddresses.join("%2C");
const price_query = `https://api.coingecko.com/api/v3/simple/token_price/${platform}?contract_addresses=${addresses}&vs_currencies=usd`;
//console.log(price_query)
try {
const tokenContractInfos = await axios.get(price_query);
data = tokenContractInfos.data;
} catch (e) {
console.log(e);
console.log("could not find prices for addresses");
}
// find prices included in exceptions using coin gecko id and add it back to the results
const filteredIds = COIN_GECKO_EXCEPTIONS.filter(
(x) => tokenAddresses.includes(x.tokenAddress) && x.chainId == chainId
);
const coinGeckoIds = filteredIds.map((x) => x.coinGeckoId);
const additional_data = await getTokenPricesCGID(coinGeckoIds);
let gecko_id_data = {};
for (let i = 0; i < filteredIds.length; i++) {
let cg_id = filteredIds[i];
gecko_id_data[cg_id.tokenAddress] = additional_data[cg_id.coinGeckoId];
}
return { ...data, ...gecko_id_data };
}
export async function getTokenPricesCGID(coinGeckoIds: String[]) {
let data;
const ids = coinGeckoIds.join("%2C");
const price_query = `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`;
// console.log(price_query);
try {
const tokenContractInfos = await axios.get(price_query);
data = tokenContractInfos.data;
} catch (e) {
console.log(e);
console.log("could not find prices for ids");
}
return data;
}

View File

@ -0,0 +1,105 @@
import { grabTerraCustodyData } from "./getTerraCustody";
import { grabSolanaCustodyData } from "./getSolanaCustody";
import { grabEvmCustodyData } from "./getEvmCustody";
import { MongoClient } from "mongodb";
interface Token {
tokenAddress: string;
name: string;
decimals: number;
symbol: string;
balance: BigInt;
qty: number;
tokenPrice: number;
tokenBalanceUSD: number;
}
interface CustodyInfo {
_id: string;
chainName: string;
chainId: number;
emitterAddress: string;
custodyUSD: number;
tokens: Token[];
}
async function updateTable(chainInfo, client: MongoClient) {
const custodyList = chainInfo.balances;
try {
const totalCustodyUSD = custodyList
.map((x) => x.tokenBalanceUSD)
.reduce((partialSum, a) => partialSum + a, 0);
console.log("totalCustodyUSD=", totalCustodyUSD);
const database = client.db("onchain_data");
// Specifying a Schema is optional, but it enables type hints on
// finds and inserts
const chainId = chainInfo.chain_id;
const emitterAddress = chainInfo.emitter_address;
const custody = database.collection<CustodyInfo>("custody");
const result = await custody.updateOne(
{ _id: `${chainId}/${emitterAddress}` },
{
$set: {
chainName: chainInfo.name,
chainId: chainId,
emitterAddress: emitterAddress,
custodyUSD: totalCustodyUSD,
tokens: custodyList,
_id: `${chainId}/${emitterAddress}`,
},
},
{ upsert: true }
);
console.log(`A document was inserted with the _id: ${result.upsertedId}`);
} catch (e) {
console.log(encodeURIComponent);
}
return;
}
const useAllowListstr = process.env.allowlist || "false";
(async () => {
const uri = process.env.MONGODB_URI;
if (uri === "" || uri === undefined) {
console.log("No mongodb uri supplied");
return -1;
}
const client = new MongoClient(uri);
const useAllowList = true ? useAllowListstr === "true" : false;
const promises = [
grabSolanaCustodyData("1", useAllowList),
grabEvmCustodyData("2", useAllowList),
grabTerraCustodyData("3", useAllowList),
grabEvmCustodyData("4", useAllowList),
grabEvmCustodyData("5", useAllowList),
grabEvmCustodyData("6", useAllowList),
grabEvmCustodyData("7", useAllowList),
// grabAlgorandCustodyData("8", useAllowList),
grabEvmCustodyData("9", useAllowList),
grabEvmCustodyData("10", useAllowList),
grabEvmCustodyData("11", useAllowList),
grabEvmCustodyData("12", useAllowList),
grabEvmCustodyData("13", useAllowList),
grabEvmCustodyData("14", useAllowList),
// grabNearustodyData("15", useAllowList),
grabEvmCustodyData("16", useAllowList),
grabTerraCustodyData("18", useAllowList),
// grabTerraCustodyData("28", useAllowList),
];
const output = await Promise.all(promises);
// iterate through chains & insert into mongodb
try {
for (let i = 0; i < output.length; i++) {
const data = output[i];
await updateTable(data, client);
}
} catch (e) {
console.log(e);
} finally {
await client.close();
}
})();

View File

@ -0,0 +1,362 @@
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) {
return allowList[chainId];
}
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);
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];
const balances = await getEvmCustody(chainInfo, useAllowList);
// 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);
// })();

View File

@ -0,0 +1,322 @@
import { formatUnits } from "ethers/lib/utils";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Connection, AccountInfo, PublicKey } from "@solana/web3.js";
import { Connection as ConnectionMeta, programs } from "@metaplex/js";
import { getTokenPricesCGID, getTokenPricesGET } from "./getCoinGeckoPrices";
import { CHAIN_INFO_MAP, DISALLOWLISTED_ADDRESSES, sleepFor } from "./utils";
// current allowlist used for stats/govenor
import allowList = require("./allowList.json");
import { getEmitterAddressSolana } from "@certusone/wormhole-sdk";
import { BigNumber } from "ethers";
require("dotenv").config();
function getAllowList(chainId) {
return allowList[chainId];
}
function calcTokenQty(tokenInfo) {
return Number(formatUnits(tokenInfo.balance, tokenInfo.decimals));
}
const {
metadata: { Metadata },
} = programs;
export async function getMultipleAccountsRPC(
connection: Connection,
pubkeys: PublicKey[]
): Promise<(AccountInfo<Buffer> | null)[]> {
return getMultipleAccounts(connection, pubkeys, "confirmed");
}
export const getMultipleAccounts = async (
connection: any,
pubkeys: PublicKey[],
commitment: string
) => {
return (
await Promise.all(connection.getMultipleAccountsInfo(pubkeys, commitment))
).flat();
};
export function shortenAddress(address: string) {
return address.length > 10
? `${address.slice(0, 4)}...${address.slice(-4)}`
: address;
}
export const METADATA_REPLACE = new RegExp("\u0000", "g");
export const EDITION_MARKER_BIT_SIZE = 248;
export const METADATA_PREFIX = "metadata";
export const EDITION = "edition";
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export type StringPublicKey = string;
export enum MetadataKey {
Uninitialized = 0,
MetadataV1 = 4,
EditionV1 = 1,
MasterEditionV1 = 2,
MasterEditionV2 = 6,
EditionMarker = 7,
}
export const METADATA_PROGRAM_ID =
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" as StringPublicKey;
export const getMetadataAddress = async (
mintKey: string
): Promise<[PublicKey, number]> => {
const seeds = [
Buffer.from("metadata"),
new PublicKey(METADATA_PROGRAM_ID).toBuffer(),
new PublicKey(mintKey).toBuffer(),
];
return PublicKey.findProgramAddress(
seeds,
new PublicKey(METADATA_PROGRAM_ID)
);
};
async function getSolanaMetaData(
mintAddresses: string[],
connection: Connection
) {
const metaAddresses = [];
for (let i = 0; i < mintAddresses.length; i++) {
let mintAddress = mintAddresses[i];
try {
const metaAddress = await getMetadataAddress(mintAddress);
metaAddresses.push({
mint: mintAddress,
meta: metaAddress[0].toString(),
});
} catch (e) {
continue;
}
}
let storeMetadata = {};
// Get store metadata
for (let i = 0; i < metaAddresses.length; i++) {
let mintkey = new PublicKey(metaAddresses[i].mint);
let pubkey = new PublicKey(metaAddresses[i].meta);
try {
const metadata = await Metadata.load(connection, pubkey);
const metadatadata = metadata.data?.data || {};
metadatadata["metakey"] = pubkey.toString();
metadatadata["tokenAddress"] = mintkey.toString();
storeMetadata[mintkey.toString()] = metadatadata;
} catch (e) {
continue;
}
}
return storeMetadata;
}
export async function getSolanaTokenAccounts(chainInfo, useAllowList) {
const chainId = chainInfo.chain_id;
const custodyAddress = chainInfo.custody_address;
const connection = new Connection(chainInfo.endpoint_url);
try {
var mintAddresses = [];
var tokenAccounts = [];
if (useAllowList) {
const allowList = getAllowList(chainId);
var mintAddresses = [];
Object.keys(allowList).forEach((address) => {
mintAddresses.push(address);
});
for (let i = 0; i < mintAddresses.length; i++) {
const mintAddress = mintAddresses[i];
const parsedAccount = await connection.getParsedTokenAccountsByOwner(
new PublicKey(custodyAddress),
{
mint: new PublicKey(mintAddress),
}
);
const tokenAccount_ = parsedAccount.value.at(-1);
const tokenAccount = tokenAccount_.account.data.parsed.info;
if (tokenAccount.tokenAmount?.amount > 0) {
// console.log(
// `token=${mintAddress} has a balance=${tokenAccount.tokenAmount?.amount}`
// );
tokenAccounts.push(tokenAccount);
} else {
console.log(`${tokenAccount} has a 0 balance`);
}
await sleepFor(1000);
}
} else {
const allAccounts = await connection.getParsedTokenAccountsByOwner(
new PublicKey(custodyAddress),
{ programId: TOKEN_PROGRAM_ID },
"confirmed"
);
allAccounts.value.forEach((account) => {
if (account.account.data.parsed?.info?.tokenAmount?.amount > 0) {
// get all token accounts with nonzero balance
tokenAccounts.push(account.account.data.parsed?.info);
}
});
// get mint addresses from token accounts and find metadata address
mintAddresses = tokenAccounts.map((x) => x.mint);
}
const storeMetadata = await getSolanaMetaData(mintAddresses, connection);
tokenAccounts = tokenAccounts.map((custody) => ({
...custody,
...custody["tokenAmount"],
...storeMetadata[custody.mint],
}));
// do a little tidying up
tokenAccounts = tokenAccounts.map((account) => ({
tokenAddress: account["tokenAddress"],
name: account["name"],
decimals: account["decimals"],
symbol: account["symbol"],
balance: BigNumber.from(account["amount"]),
}));
return tokenAccounts;
} catch (e) {
console.log(e);
}
return [];
}
async function getTokenValues(chainInfo, tokenInfos: any[], 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);
for (const [key, value] of Object.entries(prices)) {
if (!value.hasOwnProperty("usd")) {
prices[key] = { usd: 0 };
}
}
// console.log(prices);
// 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) {
const 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;
}
// console.log("tokenPrices", tokenPrices);
}
// 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)
);
var sorted = balancesUSDFiltered.sort((a, b) =>
a.tokenBalanceUSD < b.tokenBalanceUSD ? 1 : -1
);
return sorted;
} catch (e) {
console.log(e);
}
return [];
}
export async function getSolanaCustody(chainInfo, useAllowList = true) {
const endpoint = chainInfo.endpoint_url;
const bridgeAddress = chainInfo.token_bridge_address;
const connection = new Connection(endpoint);
const nativeBalance = await connection.getBalance(
new PublicKey(bridgeAddress)
);
// console.log("amount of sol in custody=", nativeBalance);
const solanaCustodyNativeUSD = 0; //solanaCustodyNative.map(x => (parseInt(x.balance) / (10.0 ** x.decimals) * x.price)).reduce((partialSum, a) => partialSum + a, 0);
// grab token accounts from rpc/allowlist
const tokenAccounts = await getSolanaTokenAccounts(chainInfo, useAllowList);
console.log(
`Num of ${chainInfo.platform} token accounts=${tokenAccounts.length}`
);
// tokenAccounts.forEach((x) => console.log(x));
const custody = await getTokenValues(chainInfo, tokenAccounts, useAllowList);
console.log(
`Num of filtered ${chainInfo.platform} token accounts=${custody.length}`
);
return custody;
}
export async function grabSolanaCustodyData(chain, useAllowList) {
const chainInfo = CHAIN_INFO_MAP[chain];
const balances = await getSolanaCustody(chainInfo, useAllowList);
const chainInfo_ = {
...chainInfo,
emitter_address:
"ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5",
balances: balances,
};
return chainInfo_;
}
// const useAllowListstr = process.env.chain || "false";
// (async () => {
// const useAllowList = true ? useAllowListstr === "true" : false;
// const chainInfo = CHAIN_INFO_MAP["1"];
// const balances = await getSolanaCustody(chainInfo, useAllowList);
// await updateTable(chainInfo, balances);
// })();

View File

@ -0,0 +1,325 @@
import { LCDClient } from "@terra-money/terra.js";
import { formatUnits } from "ethers/lib/utils";
import { getTokenPricesCGID, getTokenPricesGET } from "./getCoinGeckoPrices";
import {
CHAIN_ID_TERRA,
CHAIN_ID_TERRA2,
isNativeDenom,
getEmitterAddressTerra,
} from "@certusone/wormhole-sdk";
import axios from "axios";
import { DISALLOWLISTED_ADDRESSES, CHAIN_INFO_MAP } from "./utils";
// current allowlist used for stats/govenor
import allowList = require("./allowList.json");
import { BigNumber } from "ethers";
require("dotenv").config();
function formatTerraNative(address: string) {
let symbol = address.slice(1);
if (address != "uluna") {
symbol = symbol.slice(0, -1) + "t";
}
return symbol;
}
function getAllowList(chainId) {
return allowList[chainId];
}
function calcTokenQty(tokenInfo) {
return Number(formatUnits(tokenInfo.balance, tokenInfo.decimals));
}
async function contractQuery(
address: string,
query: object,
host
): Promise<any> {
//return this.provider.wasm.contractQuery(address, query);
const lcd = new LCDClient(host);
let info = undefined;
try {
info = await lcd.wasm.contractQuery(address, query);
} catch (e) {
console.log("could not query contract info: ", address);
}
return info;
}
async function queryTokenInfo(address: string, host): Promise<any> {
return await contractQuery(
address,
{
token_info: {},
},
host
);
}
function getNativeTerraTokenInfo(address, chainId) {
if (address === "uluna") {
if (chainId === CHAIN_ID_TERRA) {
return {
name: "Luna Classic",
symbol: "LUNC",
decimals: 6,
tokenAddress: address,
};
} else {
return {
name: "Luna",
symbol: "LUNA",
decimals: 6,
tokenAddress: address,
};
}
} else if (address === "uusd") {
return {
name: "UST",
symbol: "UST",
decimals: 6,
tokenAddress: address,
};
}
return {};
}
export async function getTerraTokenAccounts(chainInfo, useAllowList) {
const chainId = chainInfo.chain_id;
const bridgeAddress = chainInfo.token_bridge_address;
var TERRA_HOST;
var network;
if (chainId == CHAIN_ID_TERRA) {
TERRA_HOST = {
URL: "https://columbus-lcd.terra.dev",
chainID: "columbus-5",
name: "mainnet",
};
network = "classic";
} else if (chainId == CHAIN_ID_TERRA2) {
TERRA_HOST = {
URL: "https://phoenix-lcd.terra.dev",
chainID: "phoenix-1",
name: "mainnet",
};
network = "mainnet";
}
const lcd = new LCDClient(TERRA_HOST);
var tokenList = [];
var tokenData = {};
if (useAllowList) {
const allowList = getAllowList(chainId);
Object.keys(allowList).forEach((address) => {
tokenList.push(address);
});
for (let i = 0; i < tokenList.length; i++) {
let address = tokenList[i];
var tokenInfo = undefined;
if (isNativeDenom(address)) {
// console.log(address);
tokenInfo = getNativeTerraTokenInfo(address, chainId);
} else {
tokenInfo = await queryTokenInfo(address, TERRA_HOST);
}
tokenData[address] = tokenInfo;
}
// console.log("tokenData", tokenData);
} else {
const token_url = "https://assets.terra.money/cw20/tokens.json";
const response = await axios.get(token_url);
if (response.status == 200) {
const data = response["data"];
if (data.hasOwnProperty(network)) {
tokenData = data[network];
tokenList = Object.keys(tokenData);
} else {
console.log("object has no data");
}
} else {
console.log(response.status);
}
}
let tokenAccounts = [];
let nativeTokens = [];
try {
const address = bridgeAddress;
} catch (e) {
console.log(e);
}
for (let i = 0; i < tokenList.length; i++) {
const tokenAddress = tokenList[i];
try {
let token = await lcd.wasm.contractQuery(tokenAddress, {
balance: { address: bridgeAddress },
});
let tokenInfo = tokenData[tokenAddress];
// console.log(tokenAddress, token, tokenInfo);
tokenAccounts.push({
tokenAddress: tokenAddress,
name: tokenInfo.name,
decimals: tokenInfo?.decimals || 6,
symbol: tokenInfo.symbol,
balance: BigNumber.from(token["balance"]),
});
} catch (e) {
console.log("could not find address=", tokenAddress);
}
}
// grab native tokens
const [balance] = await lcd.bank.balance(bridgeAddress);
balance.toData().forEach((token) => {
if (useAllowList) {
if (tokenList.includes(token.denom)) {
let tokenInfo = tokenData[token.denom];
tokenAccounts.push({
tokenAddress: token.denom,
name: tokenInfo.name,
decimals: tokenInfo?.decimals || 6,
symbol: tokenInfo.symbol,
balance: BigNumber.from(token.amount),
});
}
} else {
tokenAccounts.push({
tokenAddress: token.denom,
name: "native",
symbol: formatTerraNative(token.denom),
balance: token.amount,
decimals: "6",
});
}
});
// console.log("tokenAccounts=", tokenAccounts);
return tokenAccounts;
}
async function getTokenValues(chainInfo, tokenInfos: any[], 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);
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];
}
}
}
} else {
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);
}
}
export async function getTerraCustody(chainInfo, useAllowList) {
const tokenAccounts = await getTerraTokenAccounts(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 grabTerraCustodyData(chain, useAllowList) {
const chainInfo = CHAIN_INFO_MAP[chain];
const balances = await getTerraCustody(chainInfo, useAllowList);
const chainInfo_ = {
...chainInfo,
emitter_address: await getEmitterAddressTerra(
chainInfo.token_bridge_address
),
balances: balances,
};
return chainInfo_;
}
// const chain = process.env.chain;
// const useAllowListstr = process.env.chain || "false";
// var func = async () => {
// const chainInfo = CHAIN_INFO_MAP[chain];
// const useAllowList = true ? useAllowListstr === "true" : false;
// const balances = await getTerraCustody(chainInfo, useAllowList);
// console.log(balances);
// await updateTable(chainInfo, balances);
// // console.log("terra custody (USD) = ", terraCustodyUSD);
// };
// func().then((x) => console.log("end"));

412
onchain_data/src/utils.ts Normal file
View File

@ -0,0 +1,412 @@
import {
CHAIN_ID_ACALA,
CHAIN_ID_ALGORAND,
CHAIN_ID_AURORA,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_CELO,
CHAIN_ID_ETH,
CHAIN_ID_ETHEREUM_ROPSTEN,
CHAIN_ID_FANTOM,
CHAIN_ID_KARURA,
CHAIN_ID_KLAYTN,
CHAIN_ID_MOONBEAM,
CHAIN_ID_NEAR,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
CHAIN_ID_TERRA2,
} from "@certusone/wormhole-sdk";
export const WORMHOLE_RPC_HOSTS = [
"https://wormhole-v2-mainnet-api.certus.one",
"https://wormhole.inotel.ro",
"https://wormhole-v2-mainnet-api.mcf.rocks",
"https://wormhole-v2-mainnet-api.chainlayer.network",
"https://wormhole-v2-mainnet-api.staking.fund",
"https://wormhole-v2-mainnet.01node.com",
];
export const CHAIN_ID_MAP = {
0: undefined,
1: CHAIN_ID_SOLANA,
2: CHAIN_ID_ETH,
3: CHAIN_ID_TERRA,
4: CHAIN_ID_BSC,
5: CHAIN_ID_POLYGON,
6: CHAIN_ID_AVAX,
7: CHAIN_ID_OASIS,
8: CHAIN_ID_ALGORAND,
9: CHAIN_ID_AURORA,
10: CHAIN_ID_FANTOM,
11: CHAIN_ID_KARURA,
12: CHAIN_ID_ACALA,
13: CHAIN_ID_KLAYTN,
14: CHAIN_ID_CELO,
15: CHAIN_ID_NEAR,
16: CHAIN_ID_MOONBEAM,
18: CHAIN_ID_TERRA2,
};
import { ethers } from "ethers";
require("dotenv").config();
export const DISALLOWLISTED_ADDRESSES = [
"0x04132bf45511d03a58afd4f1d36a29d229ccc574",
"0xa79bd679ce21a2418be9e6f88b2186c9986bbe7d",
"0x931c3987040c90b6db09981c7c91ba155d3fa31f",
"0x8fb1a59ca2d57b51e5971a85277efe72c4492983",
"0xd52d9ba6fcbadb1fe1e3aca52cbb72c4d9bbb4ec",
"0x1353c55fd2beebd976d7acc4a7083b0618d94689",
"0xf0fbdb8a402ec0fc626db974b8d019c902deb486",
"0x1fd4a95f4335cf36cac85730289579c104544328",
"0x358aa13c52544eccef6b0add0f801012adad5ee3",
"0xbe32b7acd03bcc62f25ebabd169a35e69ef17601",
"0x7ffb3d637014488b63fb9858e279385685afc1e2",
"0x337dc89ebcc33a337307d58a51888af92cfdc81b",
"0x5Cb89Ac06F34f73B1A6b8000CEb0AfBc97d58B6b",
"0xd9F0446AedadCf16A12692E02FA26C617FA4D217",
"0xD7b41531456b636641F7e867eC77120441D1E1E8",
"0x9f607027b69f6e123bc3bd56a686b735fa75f30a",
"0x2a35965bbad6fd3964ef815d011c51ab1c546e67",
"0x053c070f0923a5b770cc59d7bf74ecff991cd0b8",
"0x3dab0f14ea515d5c842b631bd6df0f7f989c47b3",
"0x7ee4f716e3c716d61f6158bde3ed5ab03fb6b90c",
"0x90285e9567be274ae892c88d3ffd76c87d6c7904",
"0x2d4678e71590c56eb37869832a3642c405e1c252", // fake saitama on poly
"0x1e49f85f8f5d4ef948ccb953c0172c648b75222f",
"0x477c7802632f0d38f285a7fd7112a66c11b99db6",
"0xdaff96cc3d5e2fa982812ec12ce74833deb51327", //fake btc on bsc
"0xe389ac691bd2b0228daffff548fbce38470373e8", //fake wrapped matic on poly
"0x7e347498dfef39a88099e3e343140ae17cde260e", //wrapped avax on bsc
"0x86812b970bbdce75b4590243ba2cbff671d0b754", //fake tether on bsc
"0x3d8babf3afd0e1bfc843f9638f650fa50ae6c22b", //fake tether on eth
"0x0749902ae8ed9c6a508271bad18f185dba7185d4", //wrapped eth on polygon
"0x8e1c62f03b995938233ffa3762bd69f889016b3c", //fake luna2.0 on bsc
].map((x) => x.toLowerCase());
export const COIN_GECKO_EXCEPTIONS = [
{
chainId: 2,
tokenAddress: "0x707f9118e33a9b8998bea41dd0d46f38bb963fc8".toLowerCase(),
coinGeckoId: "ethereum",
},
{
chainId: 2,
tokenAddress: "0xdAF566020156297E2837fDfaA6Fbba929A29461E".toLowerCase(),
coinGeckoId: "safe-coin-2",
},
{
chainId: 2,
tokenAddress: "0x5ab6A4F46Ce182356B6FA2661Ed8ebcAFce995aD".toLowerCase(),
coinGeckoId: "sportium",
},
{
chainId: 3,
tokenAddress: "uluna",
coinGeckoId: "terra-luna",
},
{
chainId: 3,
tokenAddress: "ukrw",
coinGeckoId: "terra-krw",
},
{
chainId: 7,
tokenAddress: "0x21c718c22d52d0f3a789b752d4c2fd5908a8a733",
coinGeckoId: "oasis-network", // wrapped-rose does not currently have prices
},
{
chainId: 7,
tokenAddress: "0x366ef31c8dc715cbeff5fa54ad106dc9c25c6153",
coinGeckoId: "tether",
},
{
chainId: 1,
tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
coinGeckoId: "usd-coin",
},
{
chainId: 1,
tokenAddress: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
coinGeckoId: "tether",
},
{
chainId: 11,
tokenAddress: "0x0000000000000000000500000000000000000007",
coinGeckoId: "tether",
},
{
chainId: 12,
tokenAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2",
coinGeckoId: "acala",
},
{
chainId: 12,
tokenAddress: "0x0000000000000000000100000000000000000001",
coinGeckoId: "acala-dollar",
},
{
chainId: 12,
tokenAddress: "0x0000000000000000000100000000000000000000",
coinGeckoId: "acala",
},
];
export function newProvider(
url: string,
batch: boolean = false
): ethers.providers.JsonRpcProvider | ethers.providers.JsonRpcBatchProvider {
// only support http(s), not ws(s) as the websocket constructor can blow up the entire process
// it uses a nasty setTimeout(()=>{},0) so we are unable to cleanly catch its errors
if (url.includes("http")) {
if (batch) {
return new ethers.providers.JsonRpcBatchProvider(url);
}
return new ethers.providers.JsonRpcProvider(url);
}
throw new Error("url does not start with http/https!");
}
export var CHAIN_INFO_MAP = {
"1": {
name: "solana",
evm: false,
chain_id: CHAIN_ID_SOLANA,
endpoint_url:
process.env.SOLANA_RPC || "https://solana-api.projectserum.com",
core_bridge: "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth",
token_bridge_address: "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb",
custody_address: "GugU1tP7doLeTw9hQP51xRJyS8Da1fWxuiy2rVrnMD2m",
platform: "solana",
covalentChain: 1399811149,
},
"2": {
name: "eth",
evm: true,
chain_id: CHAIN_ID_ETH,
endpoint_url: process.env.ETH_RPC || "https://rpc.ankr.com/eth",
core_bridge: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B",
token_bridge_address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585",
custody_address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585",
api_key: process.env.ETHERSCAN_API,
urlStem: `https://api.etherscan.io`,
platform: "ethereum",
covalentChain: 1,
},
"3": {
name: "terra",
evm: false,
chain_id: CHAIN_ID_TERRA,
endpoint_url: "",
core_bridge: "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5",
token_bridge_address: "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf",
custody_address: "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf",
urlStem: "https://columbus-fcd.terra.dev",
platform: "terra",
covalentChain: 3,
},
"4": {
name: "bsc",
evm: true,
chain_id: CHAIN_ID_BSC,
endpoint_url: process.env.BSC_RPC || "https://rpc.ankr.com/bsc ", //moralis_url
core_bridge: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B",
token_bridge_address: "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7",
custody_address: "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7",
api_key: process.env.BSCSCAN_API,
urlStem: `https://api.bscscan.com`,
platform: "binance-smart-chain",
covalentChain: 56,
},
"5": {
name: "polygon",
evm: true,
chain_id: CHAIN_ID_POLYGON,
endpoint_url: process.env.POLYGON_RPC || "https://rpc.ankr.com/polygon ",
core_bridge: "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7",
token_bridge_address: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE",
custody_address: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE",
api_key: process.env.POLYSCAN_API,
urlStem: `https://api.polygonscan.com`,
platform: "polygon-pos", //coingecko?,
covalentChain: 137,
},
"6": {
name: "avalanche",
evm: true,
chain_id: CHAIN_ID_AVAX,
endpoint_url: process.env.AVAX_RPC || "https://rpc.ankr.com/avalanche",
core_bridge: "0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c",
token_bridge_address: "0x0e082F06FF657D94310cB8cE8B0D9a04541d8052",
custody_address: "0x0e082F06FF657D94310cB8cE8B0D9a04541d8052",
api_key: process.env.SNOWTRACE_API,
urlStem: `https://api.snowtrace.io`,
platform: "avalanche", //coingecko?
covalentChain: 43114,
},
"7": {
name: "oasis",
evm: true,
chain_id: CHAIN_ID_OASIS,
endpoint_url: "https://emerald.oasis.dev",
core_bridge: "0xfE8cD454b4A1CA468B57D79c0cc77Ef5B6f64585",
token_bridge_address: "0x5848C791e09901b40A9Ef749f2a6735b418d7564",
custody_address: "0x5848C791e09901b40A9Ef749f2a6735b418d7564",
api_key: "",
urlStem: `https://explorer.emerald.oasis.dev`,
platform: "oasis", //coingecko?
covalentChain: 42262,
},
"8": {
name: "algorand",
evm: false,
chain_id: CHAIN_ID_ALGORAND,
endpoint_url: "https://node.algoexplorerapi.io",
core_bridge: "842125965",
token_bridge_address: "842126029",
custody_address: "842126029",
api_key: "",
urlStem: `https://algoexplorer.io`,
platform: "algorand", //coingecko?
covalentChain: undefined,
},
"9": {
name: "aurora",
evm: true,
chain_id: CHAIN_ID_AURORA,
endpoint_url: "https://mainnet.aurora.dev",
core_bridge: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E",
token_bridge_address: "0x51b5123a7b0F9b2bA265f9c4C8de7D78D52f510F",
custody_address: "0x51b5123a7b0F9b2bA265f9c4C8de7D78D52f510F",
api_key: process.env.AURORA_API,
urlStem: `https://api.aurorascan.dev`, //?module=account&action=txlist&address={addressHash}
covalentChain: 1313161554,
platform: "aurora", //coingecko?
},
"10": {
name: "fantom",
evm: true,
chain_id: CHAIN_ID_FANTOM,
endpoint_url: "https://rpc.ftm.tools",
core_bridge: "0x126783A6Cb203a3E35344528B26ca3a0489a1485",
token_bridge_address: "0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2",
custody_address: "0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2",
api_key: process.env.FTMSCAN_API,
urlStem: `https://api.FtmScan.com`,
platform: "fantom", //coingecko?
covalentChain: 250,
},
"11": {
name: "karura",
evm: true,
chain_id: CHAIN_ID_KARURA,
endpoint_url: "https://eth-rpc-karura.aca-api.network",
core_bridge: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E",
token_bridge_address: "0xae9d7fe007b3327AA64A32824Aaac52C42a6E624",
custody_address: "0xae9d7fe007b3327AA64A32824Aaac52C42a6E624",
api_key: "",
urlStem: `https://blockscout.karura.network`,
platform: "karura", //coingecko?
covalentChain: "",
},
"12": {
name: "acala",
evm: true,
chain_id: CHAIN_ID_ACALA,
endpoint_url: "https://eth-rpc-acala.aca-api.network",
core_bridge: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E",
token_bridge_address: "0xae9d7fe007b3327AA64A32824Aaac52C42a6E624",
custody_address: "0xae9d7fe007b3327AA64A32824Aaac52C42a6E624",
api_key: "",
urlStem: `https://blockscout.acala.network`,
platform: "acala", //coingecko?
covalentChain: "",
},
"13": {
name: "klaytn",
evm: true,
chain_id: CHAIN_ID_KLAYTN,
endpoint_url: "https://klaytn-mainnet-rpc.allthatnode.com:8551",
core_bridge: "0x0C21603c4f3a6387e241c0091A7EA39E43E90bb7",
token_bridge_address: "0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F",
custody_address: "0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F",
api_key: process.env.KLAYTN_API,
urlStem: `https://scope.klaytn.com`,
platform: "klay-token", //coingecko?
covalentChain: "8217",
},
"14": {
name: "celo",
evm: true,
chain_id: CHAIN_ID_CELO,
endpoint_url: "https://forno.celo.org",
core_bridge: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E",
token_bridge_address: "0x796Dff6D74F3E27060B71255Fe517BFb23C93eed",
custody_address: "0x796Dff6D74F3E27060B71255Fe517BFb23C93eed",
api_key: "",
urlStem: `https://explorer.celo.org`,
platform: "celo", //coingecko?
covalentChain: "42220",
},
"15": {
name: "near",
evm: false,
chain_id: CHAIN_ID_NEAR,
endpoint_url: "https://rpc.ankr.com/near",
core_bridge: "contract.wormhole_crypto.near",
token_bridge_address: "contract.portalbridge.near",
custody_address: "contract.portalbridge.near",
urlStem: "",
platform: "near",
covalentChain: undefined,
},
"16": {
name: "moonbeam",
evm: true,
chain_id: CHAIN_ID_MOONBEAM,
endpoint_url: "https://rpc.api.moonbeam.network",
core_bridge: "0xC8e2b0cD52Cf01b0Ce87d389Daa3d414d4cE29f3",
token_bridge_address: "0xB1731c586ca89a23809861c6103F0b96B3F57D92",
custody_address: "0xB1731c586ca89a23809861c6103F0b96B3F57D92",
api_key: process.env.MOONBEAM_API,
urlStem: "https://api-moonbeam.moonscan.io",
platform: "moonbeam",
covalentChain: 1284,
},
"18": {
name: "terra2",
evm: false,
chain_id: CHAIN_ID_TERRA2,
endpoint_url: "",
core_bridge:
"terra12mrnzvhx3rpej6843uge2yyfppfyd3u9c3uq223q8sl48huz9juqffcnhp",
token_bridge_address:
"terra153366q50k7t8nn7gec00hg66crnhkdggpgdtaxltaq6xrutkkz3s992fw9",
custody_address:
"terra153366q50k7t8nn7gec00hg66crnhkdggpgdtaxltaq6xrutkkz3s992fw9",
urlStem: "https://phoenix-fcd.terra.dev",
platform: "terra",
covalentChain: 3,
},
"2_testnet": {
name: "eth_testnet",
evm: true,
chain_id: CHAIN_ID_ETHEREUM_ROPSTEN,
endpoint_url: process.env.ETH_RPC,
core_bridge: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B",
token_bridge_address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585",
custody_address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585",
api_key: process.env.ETHERSCAN_API,
urlStem: `https://api.etherscan.io`,
platform: "ethereum",
covalentChain: 1,
},
};
export async function sleepFor(timeInMs: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeInMs);
});
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"outDir": "lib",
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["es2020"],
"skipLibCheck": true,
"allowJs": true,
"resolveJsonModule": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

View File

@ -29,12 +29,19 @@ async function paginatedFind(collection, req, filter) {
return cursor;
}
async function findAndSendMany(res, collectionName, reqForPagination, filter) {
const database = mongoClient.db("wormhole");
async function findAndSendMany(
db,
res,
collectionName,
reqForPagination,
filter,
project
) {
const database = mongoClient.db(db);
const collection = database.collection(collectionName);
const cursor = await (reqForPagination
? paginatedFind(collection, reqForPagination, filter)
: collection.find(filter));
: collection.find(filter).project(project));
const result = await cursor.toArray();
if (result.length === 0) {
res.sendStatus(404);
@ -43,10 +50,10 @@ async function findAndSendMany(res, collectionName, reqForPagination, filter) {
res.send(result);
}
async function findAndSendOne(res, collectionName, filter) {
const database = mongoClient.db("wormhole");
async function findAndSendOne(db, res, collectionName, filter, project) {
const database = mongoClient.db(db);
const collection = database.collection(collectionName);
const result = await collection.findOne(filter);
const result = await collection.findOne(filter, project);
if (!result) {
res.sendStatus(404);
return;
@ -59,7 +66,7 @@ async function findAndSendOne(res, collectionName, filter) {
*/
app.get("/api/heartbeats", async (req, res) => {
await findAndSendMany(res, "heartbeats");
await findAndSendMany("wormhole", res, "heartbeats");
});
/*
@ -67,28 +74,28 @@ app.get("/api/heartbeats", async (req, res) => {
*/
app.get("/api/vaas", async (req, res) => {
await findAndSendMany(res, "vaas", req);
await findAndSendMany("wormhole", res, "vaas", req);
});
app.get("/api/vaas/:chain", async (req, res) => {
await findAndSendMany(res, "vaas", req, {
await findAndSendMany("wormhole", res, "vaas", req, {
_id: { $regex: `^${req.params.chain}/.*` },
});
});
app.get("/api/vaas/:chain/:emitter", async (req, res) => {
await findAndSendMany(res, "vaas", req, {
await findAndSendMany("wormhole", res, "vaas", req, {
_id: { $regex: `^${req.params.chain}/${req.params.emitter}/.*` },
});
});
app.get("/api/vaas/:chain/:emitter/:sequence", async (req, res) => {
const id = `${req.params.chain}/${req.params.emitter}/${req.params.sequence}`;
await findAndSendOne(res, "vaas", { _id: id });
await findAndSendOne("wormhole", res, "vaas", { _id: id });
});
app.get("/api/vaas-sans-pythnet", async (req, res) => {
await findAndSendMany(res, "vaas", req, {
await findAndSendMany("wormhole", res, "vaas", req, {
_id: { $not: { $regex: `^26/.*` } },
});
});
@ -140,23 +147,23 @@ app.get("/api/vaa-counts", async (req, res) => {
*/
app.get("/api/observations", async (req, res) => {
await findAndSendMany(res, "observations", req);
await findAndSendMany("wormhole", res, "observations", req);
});
app.get("/api/observations/:chain", async (req, res) => {
await findAndSendMany(res, "observations", req, {
await findAndSendMany("wormhole", res, "observations", req, {
_id: { $regex: `^${req.params.chain}/.*` },
});
});
app.get("/api/observations/:chain/:emitter", async (req, res) => {
await findAndSendMany(res, "observations", req, {
await findAndSendMany("wormhole", res, "observations", req, {
_id: { $regex: `^${req.params.chain}/${req.params.emitter}/.*` },
});
});
app.get("/api/observations/:chain/:emitter/:sequence", async (req, res) => {
await findAndSendMany(res, "observations", req, {
await findAndSendMany("wormhole", res, "observations", req, {
_id: {
$regex: `^${req.params.chain}/${req.params.emitter}/${req.params.sequence}/.*`,
},
@ -167,10 +174,424 @@ app.get(
"/api/observations/:chain/:emitter/:sequence/:signer/:hash",
async (req, res) => {
const id = `${req.params.chain}/${req.params.emitter}/${req.params.sequence}/${req.params.signer}/${req.params.hash}`;
await findAndSendOne(res, "observations", { _id: id });
await findAndSendOne("wormhole", res, "observations", { _id: id });
}
);
/*
* GovernorStatus
*/
app.get("/api/governorStatus", async (req, res) => {
const database = mongoClient.db("wormhole");
const collection = database.collection("governorStatus");
const cursor = await collection.find({}).project({
createdAt: 1,
updatedAt: 1,
nodename: "$parsedStatus.nodename", //<-- rename fields to flatten
chains: "$parsedStatus.chains",
});
const result = await cursor.toArray();
if (result.length === 0) {
res.sendStatus(404);
return;
}
res.send(result);
});
app.get("/api/governorStatus/:guardianaddr", async (req, res) => {
const id = `${req.params.guardianaddr}`;
await findAndSendOne(
"wormhole",
res,
"governorStatus",
{
_id: id,
},
{
projection: {
createdAt: 1,
updatedAt: 1,
nodename: "$parsedStatus.nodename",
chains: "$parsedStatus.chains",
},
}
);
});
app.get("/api/governorStatus/chain/:chainNum", async (req, res) => {
const id = `${req.params.chainNum}`;
const database = mongoClient.db("wormhole");
const collection = database.collection("governorStatus");
const cursor = await collection.aggregate([
{
$match: {},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: "$parsedStatus.nodename",
"parsedStatus.chains": {
$filter: {
input: "$parsedStatus.chains",
as: "chain",
cond: { $eq: [`$$chain.chainid`, parseInt(id)] },
},
},
},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: 1,
availableNotional: { $arrayElemAt: ["$parsedStatus.chains", 0] },
},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: 1,
chainId: "$availableNotional.chainid",
availableNotional: "$availableNotional.remainingavailablenotional",
},
},
]);
const result = await cursor.toArray();
if (result.length === 0) {
res.sendStatus(404);
return;
}
res.send(result);
});
app.get("/api/governorStatus/chains/all", async (req, res) => {
const database = mongoClient.db("wormhole");
const collection = database.collection("governorStatus");
const cursor = await collection.aggregate([
{
$match: {},
},
{
$project: {
chains: "$parsedStatus.chains",
},
},
{
$unwind: "$chains",
},
{
$sort: {
"chains.chainid": 1,
"chains.remainingavailablenotional": -1,
},
},
{
$group: {
_id: "$chains.chainid",
availableNotionals: {
$push: {
availableNotional: "$chains.remainingavailablenotional",
},
},
},
},
{
$project: {
chainId: "$_id",
availableNotionals: 1,
},
},
]);
const result = await cursor.toArray();
if (result.length === 0) {
res.sendStatus(404);
return;
}
const minGuardianNum = 13;
var agg = [];
result.forEach((chain) => {
agg.push({
chainId: chain.chainId,
availableNotional:
chain.availableNotionals[minGuardianNum - 1]?.availableNotional || null,
});
});
res.send(
agg.sort(function (a, b) {
return parseInt(a.chainId) - parseInt(b.chainId);
})
);
});
app.get("/api/governorStatus/availableNotional/:chainNum", async (req, res) => {
const id = `${req.params.chainNum}`;
const database = mongoClient.db("wormhole");
const collection = database.collection("governorStatus");
const cursor = await collection.aggregate([
{
$match: {},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: "$parsedStatus.nodename",
"parsedStatus.chains": {
$filter: {
input: "$parsedStatus.chains",
as: "chain",
cond: { $eq: [`$$chain.chainid`, parseInt(id)] },
},
},
},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: 1,
availableNotional: { $arrayElemAt: ["$parsedStatus.chains", 0] },
},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: 1,
chainId: "$availableNotional.chainid",
availableNotional: "$availableNotional.remainingavailablenotional",
emitters: "$availableNotional.emitters",
},
},
]);
const result = await cursor.toArray();
const sortedResult = result.sort(function (b, a) {
return parseInt(a.availableNotional) - parseInt(b.availableNotional);
});
if (sortedResult.length === 0) {
res.sendStatus(404);
return;
}
const minGuardianNum = 13;
res.send(sortedResult[minGuardianNum - 1]);
});
app.get("/api/governorStatus/enqueuedVaass/all", async (req, res) => {
const id = `${req.params.chainNum}`;
const database = mongoClient.db("wormhole");
const collection = database.collection("governorStatus");
const cursor = await collection.aggregate([
{
$match: {},
},
{
$project: {
chains: "$parsedStatus.chains",
},
},
{
$unwind: "$chains",
},
{
$project: {
_id: 1,
chainId: "$chains.chainid",
emitters: "$chains.emitters",
},
},
{
$group: {
_id: "$chainId",
emitters: {
$push: {
emitterAddress: { $arrayElemAt: ["$emitters.emitteraddress", 0] },
enqueuedVaas: { $arrayElemAt: ["$emitters.enqueuedvaas", 0] },
},
},
},
},
]);
const result = await cursor.toArray();
var filteredResult = [];
var keys = [];
result.forEach((res) => {
const chainId = res._id;
const emitters = res.emitters;
emitters.forEach((emitter) => {
const emitterAddress = emitter.emitterAddress;
const enqueuedVaas = emitter.enqueuedVaas;
if (enqueuedVaas != null) {
enqueuedVaas.forEach((vaa) => {
//add to dictionary
const key = `${emitterAddress}/${vaa.sequence}/${vaa.txhash}`;
if (!keys.includes(key)) {
filteredResult.push({
chainId: chainId,
emitterAddress: emitterAddress,
sequence: vaa.sequence,
notionalValue: vaa.notionalvalue,
txHash: vaa.txhash,
});
keys.push(key);
}
});
}
});
});
if (filteredResult.length === 0) {
res.sendStatus(404);
return;
}
const groups = filteredResult.reduce((groups, item) => {
const group = groups[item.chainId] || [];
group.push(item);
groups[item.chainId] = group;
return groups;
}, {});
const modifiedResult = [];
for (const [key, value] of Object.entries(groups)) {
modifiedResult.push({ chainId: key, enqueuedVaas: value });
}
res.send(modifiedResult);
});
app.get("/api/governorStatus/enqueuedVaas/:chainNum", async (req, res) => {
const id = `${req.params.chainNum}`;
const database = mongoClient.db("wormhole");
const collection = database.collection("governorStatus");
const cursor = await collection.aggregate([
{
$match: {},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: "$parsedStatus.nodename",
"parsedStatus.chains": {
$filter: {
input: "$parsedStatus.chains",
as: "chain",
cond: { $eq: [`$$chain.chainid`, parseInt(id)] },
},
},
},
},
{
$project: {
_id: 1,
createdAt: 1,
updatedAt: 1,
nodeName: 1,
emitters: "$parsedStatus.chains.emitters",
},
},
{
$unwind: "$emitters",
},
{
$group: {
_id: { $arrayElemAt: ["$emitters.emitteraddress", 0] },
enqueuedVaas: {
$push: {
enqueuedVaa: "$emitters.enqueuedvaas",
},
},
},
},
]);
const result = await cursor.toArray();
var filteredResult = [];
var keys = [];
result.forEach((res) => {
const emitterAddress = res._id;
const enqueuedVaas = res.enqueuedVaas;
enqueuedVaas.forEach((vaa) => {
const enqueuedVaa = vaa.enqueuedVaa;
enqueuedVaa.forEach((eV) => {
if (eV != null) {
eV.forEach((ev) => {
if (ev != null) {
//add to dictionary
const key = `${emitterAddress}/${ev.sequence}/${ev.txhash}`;
if (!keys.includes(key)) {
filteredResult.push({
chainId: id,
emitterAddress: emitterAddress,
sequence: ev.sequence,
notionalValue: ev.notionalvalue,
txHash: ev.txhash,
releaseTime: ev.releasetime,
});
keys.push(key);
}
}
});
}
});
});
});
if (filteredResult.length === 0) {
res.sendStatus(404);
return;
}
const sortedResult = filteredResult.sort(function (a, b) {
return parseInt(a.sequence) - parseInt(b.sequence);
});
res.send(sortedResult);
});
/*
* Custody
*/
app.get("/api/custody", async (req, res) => {
await findAndSendMany("onchain_data", res, "custody", req);
});
app.get("/api/custody/:chain/:emitter", async (req, res) => {
const id = `${req.params.chain}/${req.params.emitter}`;
await findAndSendOne(
"onchain_data",
res,
"custody",
{
_id: id,
},
{}
);
});
app.get("/api/custody/tokens", async (req, res) => {
await findAndSendMany("onchain_data", res, "custody", req, {}, { tokens: 1 });
});
app.get("/api/custody/tokens/:chain/:emitter", async (req, res) => {
const id = `${req.params.chain}/${req.params.emitter}`;
await findAndSendOne(
"onchain_data",
res,
"custody",
{
_id: id,
},
{ projection: { tokens: 1 } }
);
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});

View File

@ -20,6 +20,10 @@ import CustomThemeProvider from "./components/CustomThemeProvider";
import ErrorFallback from "./components/ErrorFallback";
import Governance from "./components/Governance";
import Guardians from "./components/Guardians";
// import Governor from "./components/Governor";
// import GovernorStatus from "./components/GovernorStatus";
import CustodyData from "./components/Custody";
import Home from "./components/Home";
import VAAs from "./components/VAAs";
import { NetworkContextProvider } from "./contexts/NetworkContext";
@ -55,6 +59,12 @@ function App() {
<Button component={Link} to="/guardians">
Guardians
</Button>
<Button component={Link} to="/custody">
Custody
</Button>
{/* <Button component={Link} to="/governorStatus">
Governor Status
</Button> */}
</Box>
<Box flexGrow={1} />
<Box>
@ -74,6 +84,12 @@ function App() {
<Route exact path="/guardians">
<Guardians />
</Route>
<Route exact path="/custody">
<CustodyData />
</Route>
{/* <Route exact path="/governorStatus">
<GovernorStatus />
</Route> */}
<Route exact path="/governance">
<Governance />
</Route>

View File

@ -0,0 +1,122 @@
import { ChevronRight } from "@mui/icons-material";
import { Box } from "@mui/system";
import { Card, IconButton, Typography } from "@mui/material";
import {
createColumnHelper,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
getExpandedRowModel,
Row,
} from "@tanstack/react-table";
import numeral from "numeral";
import { useState, ReactElement } from "react";
import useCustodyData, { CustodyDataResponse } from "../hooks/useCustodyData";
import Table from "./Table";
import TokenDetails from "./TokenDetails";
const columnHelper = createColumnHelper<CustodyDataResponse>();
const columns = [
columnHelper.display({
id: "_expand",
cell: ({ row }) =>
row.getCanExpand() ? (
<IconButton
size="small"
{...{
onClick: row.getToggleExpandedHandler(),
style: { cursor: "pointer" },
}}
>
<ChevronRight
sx={{
transition: ".2s",
transform: row.getIsExpanded() ? "rotate(90deg)" : undefined,
}}
/>
</IconButton>
) : null,
}),
columnHelper.accessor("chainId", {
header: () => "Chain Id",
sortingFn: `text`,
}),
columnHelper.accessor("chainName", {
header: () => "Chain Name",
}),
columnHelper.accessor("custodyUSD", {
header: () => "Total Value Locked (USD)",
cell: (info) => (
<Box textAlign="left">${numeral(info.getValue()).format("0,0.0000")}</Box>
),
}),
columnHelper.accessor("tokens", {
header: () => "Locked Tokens",
cell: (info) => {
const value = info.getValue();
return `${value.length} Token` + (value.length == 1 ? "" : `s`);
},
}),
];
/*
interface Token {
tokenAddress: string;
name: string;
decimals: number;
symbol: string;
balance: BigInt;
qty: number;
tokenPrice: number;
tokenBalanceUSD: number;
}
*/
function AddTokenDetails({
row,
}: {
row: Row<CustodyDataResponse>;
}): ReactElement {
const id = row.original._id;
return TokenDetails(id);
}
function CustodyData() {
const custody = useCustodyData();
const [sorting, setSorting] = useState<SortingState>([]);
const lockedValue = custody
.map((x) => x.custodyUSD)
.reduce((partialSum, a) => partialSum + a, 0);
const table = useReactTable({
columns,
data: custody,
state: {
sorting,
},
getRowId: (chain) => chain.chainName,
getRowCanExpand: () => true,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
});
return (
<Box m={2}>
<Card>
<Box m={2}>
Total Locked Value (USD): ${numeral(lockedValue).format("0,0.0000")}
</Box>
<Table<CustodyDataResponse>
table={table}
renderSubComponent={AddTokenDetails}
/>
</Card>
</Box>
);
}
export default CustodyData;

View File

@ -0,0 +1,84 @@
import { Box, Card } from "@mui/material";
import {
createColumnHelper,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import numeral from "numeral";
import { useState } from "react";
import useTokenDetails, {
TokenDetailsResponse,
} from "../hooks/useTokenDetails";
import Table from "./Table";
/*
export type TokenDetailsResponse {
tokenAddress: string;
name: string;
decimals: number;
symbol: string;
balance: BigInt;
qty: number;
tokenPrice: number;
tokenBalanceUSD: number;
}
*/
const columnHelper = createColumnHelper<TokenDetailsResponse>();
const columns = [
columnHelper.accessor("tokenAddress", {
header: () => "Token Address",
sortingFn: `text`,
}),
columnHelper.accessor("name", {
header: () => "Name",
}),
columnHelper.accessor("symbol", {
header: () => "Symbol",
}),
columnHelper.accessor("qty", {
header: () => "Token Balance",
cell: (info) => (
<Box textAlign="left">{numeral(info.getValue()).format("0,0.0000")}</Box>
),
}),
columnHelper.accessor("tokenPrice", {
header: () => "Token Price",
cell: (info) => (
<Box textAlign="left">${numeral(info.getValue()).format("0,0.0000")}</Box>
),
}),
columnHelper.accessor("tokenBalanceUSD", {
header: () => "Locked Value (USD)",
cell: (info) => (
<Box textAlign="left">${numeral(info.getValue()).format("0,0.0000")}</Box>
),
}),
];
function TokenDetails(id: string) {
const tokenDetails = useTokenDetails(id);
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
columns,
data: tokenDetails,
state: {
sorting,
},
getRowId: (token) => token.name,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
});
return (
<Box m={2}>
<Card>
<Table<TokenDetailsResponse> table={table} />
</Card>
</Box>
);
}
export default TokenDetails;

View File

@ -0,0 +1,49 @@
import axios from "axios";
import { useEffect, useState } from "react";
import { useNetworkContext } from "../contexts/NetworkContext";
import { POLL_TIME } from "../utils/consts";
export type Token = {
tokenAddress: string;
name: string;
decimals: number;
symbol: string;
balance: BigInt;
qty: number;
tokenPrice: number;
tokenBalanceUSD: number;
};
export type CustodyDataResponse = {
_id: string;
chainId: number;
chainName: string;
custodyUSD: number;
emitterAddress: string;
tokens: Token[];
};
function useCustodyData(): CustodyDataResponse[] {
const { currentNetwork } = useNetworkContext();
const [custodyData, setCustodyData] = useState<CustodyDataResponse[]>([]);
useEffect(() => {
setCustodyData([]);
}, [currentNetwork]);
useEffect(() => {
let cancelled = false;
(async () => {
while (!cancelled) {
const response = await axios.get<CustodyDataResponse[]>(`/api/custody`);
if (!cancelled) {
setCustodyData(response.data);
await new Promise((resolve) => setTimeout(resolve, POLL_TIME));
}
}
})();
return () => {
cancelled = true;
};
}, [currentNetwork]);
return custodyData;
}
export default useCustodyData;

View File

@ -0,0 +1,47 @@
import axios from "axios";
import { useEffect, useState } from "react";
import { useNetworkContext } from "../contexts/NetworkContext";
import { POLL_TIME } from "../utils/consts";
export type TokenDetailsResponse = {
tokenAddress: string;
name: string;
decimals: number;
symbol: string;
balance: BigInt;
qty: number;
tokenPrice: number;
tokenBalanceUSD: number;
};
export type TokensResponse = {
_id: string;
tokens: TokenDetailsResponse[];
};
function useTokenDetails(id?: string): TokenDetailsResponse[] {
const { currentNetwork } = useNetworkContext();
const [tokenDetails, setTokenDetails] = useState<TokenDetailsResponse[]>([]);
useEffect(() => {
setTokenDetails([]);
}, [currentNetwork]);
useEffect(() => {
let cancelled = false;
(async () => {
while (!cancelled) {
const response = await axios.get<TokensResponse>(
`/api/custody/tokens${id ? `/${id}` : ""}`
);
if (!cancelled) {
setTokenDetails(response.data.tokens);
await new Promise((resolve) => setTimeout(resolve, POLL_TIME));
}
}
})();
return () => {
cancelled = true;
};
}, [currentNetwork, id]);
return tokenDetails;
}
export default useTokenDetails;

View File

@ -16,6 +16,7 @@ import {
CHAIN_ID_TERRA2,
ChainId,
CHAIN_ID_NEAR,
CHAIN_ID_MOONBEAM,
} from "@certusone/wormhole-sdk";
require("dotenv").config();
@ -204,6 +205,17 @@ export const CHAIN_INFO_MAP: { [key: string]: CHAIN_INFO } = {
covalentChain: 0,
explorerStem: `https://explorer.near.org`,
},
16: {
name: "moonbeam",
evm: true,
chainId: CHAIN_ID_MOONBEAM,
endpointUrl: "https://rpc.ankr.com/moonbeam",
apiKey: "",
urlStem: `https://api-moonbeam.moonscan.io`,
platform: "moonbeam", //coingecko?
covalentChain: 0,
explorerStem: `https://moonscan.io/`,
},
18: {
name: "terra2",
evm: false,
@ -216,3 +228,32 @@ export const CHAIN_INFO_MAP: { [key: string]: CHAIN_INFO } = {
explorerStem: `https://finder.terra.money/mainnet`,
},
};
export const WORMHOLE_RPC_HOSTS = [
"https://wormhole-v2-mainnet-api.certus.one",
"https://wormhole.inotel.ro",
"https://wormhole-v2-mainnet-api.mcf.rocks",
"https://wormhole-v2-mainnet-api.chainlayer.network",
"https://wormhole-v2-mainnet-api.staking.fund",
"https://wormhole-v2-mainnet.01node.com",
];
export const CHAIN_ID_MAP = {
"1": CHAIN_ID_SOLANA,
"2": CHAIN_ID_ETH,
"3": CHAIN_ID_TERRA,
"4": CHAIN_ID_BSC,
"5": CHAIN_ID_POLYGON,
"6": CHAIN_ID_AVAX,
"7": CHAIN_ID_OASIS,
"8": CHAIN_ID_ALGORAND,
"9": CHAIN_ID_AURORA,
"10": CHAIN_ID_FANTOM,
"11": CHAIN_ID_KARURA,
"12": CHAIN_ID_ACALA,
"13": CHAIN_ID_KLAYTN,
"14": CHAIN_ID_CELO,
"15": CHAIN_ID_NEAR,
"16": CHAIN_ID_MOONBEAM,
"18": CHAIN_ID_TERRA2,
};