Skip to content

Commit

Permalink
feat: add custom logic for TVL (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
nrsirapop authored Sep 12, 2024
1 parent 0da67ee commit cf126e7
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 21 deletions.
42 changes: 42 additions & 0 deletions config/custom_tvl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
############################
# Custom TVL Configuration #
############################

mainnet:
custom_contracts:
-
address: "0xBa5E35E26Ae59c7aea6F029B68c6460De2d13eB6"
chain: "ethereum"
assets:
-
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
symbol: "USDC"
decimals: 6
image: "/logos/assets/usdc.svg"
coingecko_id: "usd-coin"
-
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7"
symbol: "USDT"
decimals: 6
image: "/logos/assets/usdt.svg"
coingecko_id: "tether"
-
address: "0x0000000000000000000000000000000000000000"
symbol: "ETH"
decimals: 18
image: "/logos/assets/eth.svg"
coingecko_id: "ethereum"
-
address: "0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF"
symbol: "IMX"
decimals: 18
image: "/logos/assets/imx.svg"
coingecko_id: "immutable-x"
custom_tokens:
-
symbol: "wstETH"
decimals: 18
image: "/logos/assets/wsteth.svg"
coingecko_id: "staked-ether"
addresses:
binance: "0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C"
8 changes: 8 additions & 0 deletions config/methods.yml
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,14 @@ methods:
name: "total_on_cosmos"
type: "float"
description: "total wrapped supply moved to Cosmos chains"
-
name: "total_on_contracts"
type: "float"
description: "total balance on custom contracts"
-
name: "total_on_tokens"
type: "float"
description: "total supply on custom tokens"
-
name: "price"
type: "float"
Expand Down
9 changes: 7 additions & 2 deletions methods/tokens/getTokensPrice.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ const _ = require('lodash');
const moment = require('moment');

const { get, write } = require('../../services/indexer');
const { TOKEN_PRICE_COLLECTION, PRICE_ORACLE_API, CURRENCY, getAssetsList, getAssetData, getITSAssetsList, getITSAssetData, getTokens } = require('../../utils/config');
const { TOKEN_PRICE_COLLECTION, PRICE_ORACLE_API, CURRENCY, getAssetsList, getAssetData, getITSAssetsList, getITSAssetData, getTokens, getCustomTVLConfig } = require('../../utils/config');
const { request } = require('../../utils/http');
const { toArray } = require('../../utils/parser');
const { equalsIgnoreCase, lastString } = require('../../utils/string');
const { isNumber, toNumber } = require('../../utils/number');
const { timeDiff } = require('../../utils/time');

const tokens = getTokens();
const { custom_contracts, custom_tokens } = { ...getCustomTVLConfig() };

const getTokenConfig = async (symbol, additionalAssetsData, notGetAssetConfig = false) => {
const tokenData = tokens[symbol] || _.last(Object.entries(tokens).find(([k, v]) => equalsIgnoreCase(k, lastString(symbol, '/')))) || (!notGetAssetConfig ? await getAssetData(symbol, additionalAssetsData) || await getITSAssetData(symbol, additionalAssetsData) : undefined);
const tokenData = tokens[symbol] ||
_.last(Object.entries(tokens).find(([k, v]) => equalsIgnoreCase(k, lastString(symbol, '/')))) ||
_.head(toArray(toArray(custom_contracts).map(c => toArray(c.assets).find(a => a.symbol === symbol && a.coingecko_id)))) ||
toArray(custom_tokens).find(c => c.symbol === symbol && c.coingecko_id) ||
(!notGetAssetConfig ? await getAssetData(symbol, additionalAssetsData) || await getITSAssetData(symbol, additionalAssetsData) : undefined);
const { redirect } = { ...tokenData };
return { ...(redirect ? await getTokenConfig(redirect, additionalAssetsData, notGetAssetConfig) : tokenData) };
};
Expand Down
40 changes: 32 additions & 8 deletions methods/tvl/getTVL.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { getTokensPrice, getTokenCirculatingSupply } = require('../tokens');
const { get, read, write } = require('../../services/indexer');
const { getBalance, getTokenSupply } = require('../../utils/chain/evm');
const { getCosmosBalance, getIBCSupply } = require('../../utils/chain/cosmos');
const { IBC_CHANNEL_COLLECTION, TVL_COLLECTION, getChainsList, getChainData, getAxelarConfig, getAssetsList, getAssetData, getITSAssetsList, getITSAssetData, getContracts, getTVLConfig } = require('../../utils/config');
const { IBC_CHANNEL_COLLECTION, TVL_COLLECTION, getChainsList, getChainData, getAxelarConfig, getAssetsList, getAssetData, getITSAssetsList, getITSAssetData, getContracts, getTVLConfig, getCustomTVLConfig } = require('../../utils/config');
const { toHash, getAddress, split, toArray } = require('../../utils/parser');
const { isString, lastString } = require('../../utils/string');
const { isNumber, toNumber } = require('../../utils/number');
Expand All @@ -17,9 +17,24 @@ const CACHE_AGE_SECONDS = 60 * 60;
const IBC_CHANNELS_UPDATE_INTERVAL_SECONDS = 240 * 60;

const normalizeCacheId = id => isString(id) ? split(id, { delimiter: '/' }).join('_') : undefined;
const generateDenom = d => `${d.decimals === 6 ? 'u' : ''}${d.symbol.toLowerCase()}${d.decimals === 18 ? '-wei' : ''}`;

module.exports = async params => {
const assetsData = toArray(await getAssetsList());
const { percent_diff_escrow_supply_threshold, percent_diff_total_supply_threshold } = { ...getTVLConfig() };
const { custom_contracts, custom_tokens } = { ...getCustomTVLConfig() };

let assetsData = toArray(await getAssetsList());
const assetsFromCustomContracts = Object.values(_.groupBy(_.uniqBy(toArray(custom_contracts).flatMap(c => toArray(c.assets).filter(a => assetsData.findIndex(d => d.symbol === a.symbol) < 0).map(a => ({ ...a, chain: c.chain, k: `${a.symbol}_${c.chain}` }))), 'k'), 'symbol')).map(v => {
const d = { ..._.head(v) };
const denom = generateDenom(d);
return { id: denom, denom, ...d, addresses: Object.fromEntries(v.map(c => [c.chain, c])) };
});
const assetsFromCustomTokens = toArray(custom_tokens).filter(c => assetsData.findIndex(d => d.symbol === c.symbol) < 0).map(d => {
const denom = generateDenom(d);
return { id: denom, denom, ...d };
});
assetsData = _.concat(assetsData, Object.values(_.groupBy(_.concat(assetsFromCustomContracts, assetsFromCustomTokens), 'id')).map(v => ({ ..._.head(v), addresses: _.merge(v.map(d => d.addresses)) })));

const itsAssetsData = toArray(await getITSAssetsList());
const { gateway_contracts } = { ...await getContracts() };
const { asset, chain, force_update } = { ...params };
Expand All @@ -29,7 +44,6 @@ module.exports = async params => {
chains = toArray(chains || chain);
chains = chains.length === 0 ? getChainsList().filter(d => (d.chain_type === 'cosmos' || gateway_contracts?.[d.id]?.address) && !d.no_tvl).map(d => d.id) : _.uniq(_.concat('axelarnet', toArray(chains.map(d => getChainData(d)?.id))));

const { percent_diff_escrow_supply_threshold, percent_diff_total_supply_threshold } = { ...getTVLConfig() };
const evmChainsData = getChainsList('evm').filter(d => chains.includes(d.id) && !d.no_tvl);
const cosmosChainsData = getChainsList('cosmos').filter(d => chains.includes(d.id) && !d.no_tvl);
const hasAllEVMChains = evmChainsData.length >= getChainsList('evm').filter(d => gateway_contracts?.[d.id]?.address && !d.no_tvl).length;
Expand All @@ -49,7 +63,7 @@ module.exports = async params => {
else if (assets.length > 1 && hasAllChains) {
const response = await read(TVL_COLLECTION, {
bool: {
should: assets.map(id => { return { match: { _id: normalizeCacheId(id) } }; }),
should: assets.map(id => ({ match: { _id: normalizeCacheId(id) } })),
minimum_should_match: 1,
},
}, { size: assets.length });
Expand Down Expand Up @@ -128,10 +142,18 @@ module.exports = async params => {
const isLockUnlock = assetType === 'its' && token_manager_address && token_manager_type?.startsWith('lockUnlock');
const token_manager_balance = isLockUnlock ? toNumber(await getBalance(id, token_manager_address, contract_data)) : 0;
const supply = !isNative || assetType === 'its' ? isLockUnlock ? token_manager_balance : toNumber(await getTokenSupply(id, contract_data)) : 0;
const custom_contracts_balance = await Promise.all(toArray(custom_contracts).filter(c => c.chain === id && c.address && toArray(c.assets).findIndex(a => a.symbol === assetData?.symbol && a.address) > -1).map(c => ({ address: c.address, balance: toNumber(await getBalance(id, c.address, { ...c.assets.find(a => a.symbol === assetData?.symbol) })), url: url && `${url}${(c.assets.find(a => a.symbol === assetData?.symbol)?.address === ZeroAddress ? address_path : contract_path).replace('{address}', c.assets.find(a => a.symbol === assetData?.symbol)?.address === ZeroAddress ? c.address : `${c.assets.find(a => a.symbol === assetData?.symbol)?.address}?a=${c.address}`)}` })));
const total_balance_on_custom_contracts = _.sumBy(custom_contracts_balance, 'balance');
const custom_tokens_supply = await Promise.all(toArray(custom_tokens).filter(c => c.symbol === assetData?.symbol && c.addresses?.[id]).map(c => ({ address: c.addresses[id], supply: toNumber(await getTokenSupply(id, { ...c, address: c.addresses[id] })), url: url && `${url}${(c.addresses[id] === ZeroAddress ? address_path : contract_path).replace('{address}', c.addresses[id])}` })));
const total_supply_of_custom_tokens = _.sumBy(custom_tokens_supply, 'supply');
result = {
contract_data, gateway_address, gateway_balance,
...(isLockUnlock ? { token_manager_address, token_manager_type, token_manager_balance } : undefined),
supply, total: isNativeOnCosmos ? 0 : gateway_balance + supply,
custom_contracts_balance,
total_balance_on_custom_contracts,
custom_tokens_supply,
total_supply_of_custom_tokens,
url: url && `${url}${(address === ZeroAddress ? address_path : contract_path).replace('{address}', address === ZeroAddress ? gateway_address : address)}${isNative && address !== ZeroAddress ? gateway_address && assetType !== 'its' ? `?a=${gateway_address}` : isLockUnlock ? `?a=${token_manager_address}` : '' : ''}`,
success: isNumber(isNative && assetType !== 'its' ? gateway_balance : supply),
};
Expand All @@ -153,7 +175,7 @@ module.exports = async params => {
const { data } = { ...await read(IBC_CHANNEL_COLLECTION, {
bool: {
must: [{ match: { state: 'STATE_OPEN' } }],
should: toArray(prefix_chain_ids).map(p => { return { match_phrase_prefix: { chain_id: p } }; }),
should: toArray(prefix_chain_ids).map(p => ({ match_phrase_prefix: { chain_id: p } })),
minimum_should_match: 1,
},
}, { size: 500, sort: [{ updated_at: 'asc' }] }) };
Expand Down Expand Up @@ -227,19 +249,21 @@ module.exports = async params => {

let total_on_evm = _.sum(toArray(Object.entries(tvl).filter(([k, v]) => getChainData(k)?.chain_type === 'evm' && (assetType !== 'its' || isCanonicalITS) && !v.token_manager_type?.startsWith('lockUnlock')).map(([k, v]) => v.supply)));
const total_on_cosmos = _.sum(toArray(Object.entries(tvl).filter(([k, v]) => getChainData(k)?.chain_type === 'cosmos' && k !== native_chain && (assetType !== 'its' || isCanonicalITS)).map(([k, v]) => v[hasAllCosmosChains ? isNativeOnCosmos || k === 'secret-snip' ? 'supply' : 'total' : 'escrow_balance'])));
const total = isNativeOnCosmos || isNativeOnAxelarnet ? total_on_evm + total_on_cosmos : assetType === 'its' ? isCanonicalITS ? _.sum(toArray(Object.values(tvl).map(d => d.token_manager_balance))) : toNumber(await getTokenCirculatingSupply(coingecko_id)) : _.sum(toArray(Object.entries(tvl).map(([k, v]) => isNativeOnEVM ? v.gateway_balance : v.total)));
const total_on_contracts = _.sum(toArray(Object.entries(tvl).filter(([k, v]) => getChainData(k)?.chain_type === 'evm' && assetType !== 'its').map(([k, v]) => v.total_balance_on_custom_contracts)));
const total_on_tokens = _.sum(toArray(Object.entries(tvl).filter(([k, v]) => getChainData(k)?.chain_type === 'evm' && assetType !== 'its').map(([k, v]) => v.total_supply_of_custom_tokens)));
const total = (isNativeOnCosmos || isNativeOnAxelarnet ? total_on_evm + total_on_cosmos : assetType === 'its' ? isCanonicalITS ? _.sum(toArray(Object.values(tvl).map(d => d.token_manager_balance))) : toNumber(await getTokenCirculatingSupply(coingecko_id)) : _.sum(toArray(Object.entries(tvl).map(([k, v]) => isNativeOnEVM ? v.gateway_balance : v.total)))) + total_on_contracts + total_on_tokens;
if (assetType === 'its' && !isCanonicalITS && isNativeOnEVM) total_on_evm += total;

const evm_escrow_address = isNativeOnCosmos ? getAddress(isNativeOnAxelarnet ? asset : `ibc/${toHash(`transfer/${_.last(tvl[native_chain]?.ibc_channels)?.channel_id}/${asset}`)}`, axelarnet.prefix_address, 32) : undefined;
const evm_escrow_balance = evm_escrow_address ? toNumber(await getCosmosBalance('axelarnet', evm_escrow_address, { ...assetData, ...addresses?.axelarnet })) : 0;
const evm_escrow_address_urls = evm_escrow_address && toArray([axelarnet.explorer?.url && axelarnet.explorer.address_path && `${axelarnet.explorer.url}${axelarnet.explorer.address_path.replace('{address}', evm_escrow_address)}`, `${axelarnetLCDUrl}/cosmos/bank/v1beta1/balances/${evm_escrow_address}`]);
const percent_diff_supply = evm_escrow_address ? evm_escrow_balance > 0 && total_on_evm > 0 ? Math.abs(evm_escrow_balance - total_on_evm) * 100 / evm_escrow_balance : null : total > 0 && total_on_evm >= 0 && total_on_cosmos >= 0 && total_on_evm + total_on_cosmos > 0 ? Math.abs(total - (total_on_evm + total_on_cosmos)) * 100 / total : null;
const percent_diff_supply = evm_escrow_address ? evm_escrow_balance > 0 && total_on_evm > 0 ? Math.abs(evm_escrow_balance - total_on_evm) * 100 / evm_escrow_balance : null : total > 0 && total_on_evm >= 0 && total_on_cosmos >= 0 && total_on_evm + total_on_cosmos > 0 ? Math.abs(total - (total_on_evm + total_on_cosmos) - (total_on_contracts + total_on_tokens)) * 100 / (total - (total_on_contracts + total_on_tokens)) : null;

const pricesData = await getTokensPrice({ symbol: asset });
const { price } = { ...(pricesData?.[asset] || pricesData?.[assetData?.symbol] || Object.values({ ...pricesData }).find(d => d.denom === asset)) };
data.push({
asset, assetType, price,
tvl, total_on_evm, total_on_cosmos, total,
tvl, total_on_evm, total_on_cosmos, total_on_contracts, total_on_tokens, total,
evm_escrow_address, evm_escrow_balance, evm_escrow_address_urls,
percent_diff_supply, is_abnormal_supply: percent_diff_supply > (evm_escrow_address ? percent_diff_escrow_supply_threshold : percent_diff_total_supply_threshold),
percent_diff_escrow_supply_threshold, percent_diff_total_supply_threshold,
Expand Down
8 changes: 4 additions & 4 deletions methods/tvl/getTVLAlert.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ module.exports = async params => {
const { updated_at } = { ..._.head(data) };

data = _.orderBy(toArray(toArray(data).map(d => _.head(toArray(d.data).filter(d => d.assetType !== 'its')))).map(d => {
const { price, total, percent_diff_supply } = { ...d };
return { ...d, value: toNumber(total * price), value_diff: toNumber(total * (percent_diff_supply / 100) * price) };
const { price, total, total_on_contracts, total_on_tokens, percent_diff_supply } = { ...d };
return { ...d, value: toNumber(total * price), value_diff: toNumber((total - (total_on_contracts + total_on_tokens)) * (percent_diff_supply / 100) * price) };
}), ['value_diff', 'value', 'total'], ['desc', 'desc', 'desc']);

const toAlertData = data.filter(d => (d.is_abnormal_supply && d.value_diff > alert_asset_value_threshold) || (
Expand All @@ -46,7 +46,7 @@ module.exports = async params => {
if (data.length > 0) {
const assetsData = await getAssetsList();
details = await Promise.all(data.map(d => new Promise(async resolve => {
const { asset, price, is_abnormal_supply, percent_diff_supply, total, value_diff, total_on_evm, total_on_cosmos, evm_escrow_address, evm_escrow_balance, evm_escrow_address_urls, tvl } = { ...d };
const { asset, price, is_abnormal_supply, percent_diff_supply, total, value_diff, total_on_evm, total_on_cosmos, total_on_contracts, total_on_tokens, evm_escrow_address, evm_escrow_balance, evm_escrow_address_urls, tvl } = { ...d };
const { native_chain, symbol, addresses } = { ...await getAssetData(asset, assetsData) };
const { chain_type } = { ...getChainData(native_chain) };
const app = getAppURL();
Expand All @@ -58,7 +58,7 @@ module.exports = async params => {
...(is_abnormal_supply && value_diff > alert_asset_value_threshold ?
{
percent_diff_supply,
total, total_on_evm, total_on_cosmos,
total, total_on_evm, total_on_cosmos, total_on_contracts, total_on_tokens,
evm_escrow_address, evm_escrow_balance,
links: _.uniq(toArray(_.concat(
evm_escrow_address_urls,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "axelarscan-api",
"version": "0.0.72",
"version": "0.0.73",
"description": "Axelarscan API",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion terraform/devnet-amplifier/variables.tf.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ variable "log_level" {

variable "app_version" {
description = "App version, same as docker image version"
default = "0.0.72"
default = "0.0.73"
validation {
error_message = "Must be valid semantic version. $Major.$Minor.$Patch"
condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", var.app_version))
Expand Down
2 changes: 1 addition & 1 deletion terraform/devnet-verifiers/variables.tf.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ variable "log_level" {

variable "app_version" {
description = "App version, same as docker image version"
default = "0.0.72"
default = "0.0.73"
validation {
error_message = "Must be valid semantic version. $Major.$Minor.$Patch"
condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", var.app_version))
Expand Down
2 changes: 1 addition & 1 deletion terraform/mainnet/variables.tf.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ variable "log_level" {

variable "app_version" {
description = "App version, same as docker image version"
default = "0.0.72"
default = "0.0.73"
validation {
error_message = "Must be valid semantic version. $Major.$Minor.$Patch"
condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", var.app_version))
Expand Down
2 changes: 1 addition & 1 deletion terraform/stagenet/variables.tf.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ variable "log_level" {

variable "app_version" {
description = "App version, same as docker image version"
default = "0.0.72"
default = "0.0.73"
validation {
error_message = "Must be valid semantic version. $Major.$Minor.$Patch"
condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", var.app_version))
Expand Down
2 changes: 1 addition & 1 deletion terraform/testnet/variables.tf.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ variable "log_level" {

variable "app_version" {
description = "App version, same as docker image version"
default = "0.0.72"
default = "0.0.73"
validation {
error_message = "Must be valid semantic version. $Major.$Minor.$Patch"
condition = can(regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", var.app_version))
Expand Down
Loading

0 comments on commit cf126e7

Please sign in to comment.