diff --git a/indexers/enterprise-stats/src/dao/getDaoAssets.ts b/indexers/enterprise-stats/src/dao/getDaoAssets.ts index 994cc842..0f3a54a0 100644 --- a/indexers/enterprise-stats/src/dao/getDaoAssets.ts +++ b/indexers/enterprise-stats/src/dao/getDaoAssets.ts @@ -5,7 +5,7 @@ import { contractQuery } from "chain/lcd" import { enterprise, enterprise_factory } from "types/contracts"; import { Dao } from "./Dao"; import { getAssetPrice } from "chain/getAssetPrice"; -import { getDaoTotalStakedAmount } from "./getDaoTotalStakedAmount"; +import { getDaoTotalStakedAmount } from "../../../enterprise/src/treasury/getDaoTotalStakedAmount"; import Big from "big.js"; const toAsset = (response: enterprise.AssetInfoBaseFor_Addr | enterprise_factory.AssetInfoBaseFor_Addr): Asset | undefined => { diff --git a/indexers/enterprise-stats/src/dao/getNFTDaoStakedValue.ts b/indexers/enterprise-stats/src/dao/getNFTDaoStakedValue.ts index ceace72b..88490893 100644 --- a/indexers/enterprise-stats/src/dao/getNFTDaoStakedValue.ts +++ b/indexers/enterprise-stats/src/dao/getNFTDaoStakedValue.ts @@ -1,5 +1,5 @@ import { Dao } from "./Dao" -import { getDaoTotalStakedAmount } from "./getDaoTotalStakedAmount" +import { getDaoTotalStakedAmount } from "../../../enterprise/src/treasury/getDaoTotalStakedAmount" import { getNFTCollectionFloorPrice } from "chain/getNftCollectionFloorPrice" export const getNFTDaoStakedValue = async (dao: Pick) => { diff --git a/indexers/enterprise-stats/src/dao/getTokenDaoStakedAsset.ts b/indexers/enterprise-stats/src/dao/getTokenDaoStakedAsset.ts index 50fcbfb1..444b2f7f 100644 --- a/indexers/enterprise-stats/src/dao/getTokenDaoStakedAsset.ts +++ b/indexers/enterprise-stats/src/dao/getTokenDaoStakedAsset.ts @@ -1,6 +1,6 @@ import { getAssetInfo } from "chain/getAssetInfo" import { Dao } from "./Dao" -import { getDaoTotalStakedAmount } from "./getDaoTotalStakedAmount" +import { getDaoTotalStakedAmount } from "../../../enterprise/src/treasury/getDaoTotalStakedAmount" import { getAssetPrice } from "chain/getAssetPrice" import { Asset, AssetWithPrice } from "chain/Asset" diff --git a/indexers/enterprise/src/chain/Asset.ts b/indexers/enterprise/src/chain/Asset.ts new file mode 100644 index 00000000..b89b177e --- /dev/null +++ b/indexers/enterprise/src/chain/Asset.ts @@ -0,0 +1,19 @@ +type AssetType = 'cw20' | 'native' + +export interface Asset { + type: AssetType + id: string +} + +export interface AssetInfo { + name: string + symbol?: string + decimals: number + icon?: string +} + +export type AssetWithInfoAndBalance = Asset & AssetInfo & { balance: string } + +export const areSameAsset = (a: Asset, b: Asset) => { + return a.type === b.type && a.id === b.id +} \ No newline at end of file diff --git a/indexers/enterprise/src/chain/NFT.ts b/indexers/enterprise/src/chain/NFT.ts new file mode 100644 index 00000000..59dc26e3 --- /dev/null +++ b/indexers/enterprise/src/chain/NFT.ts @@ -0,0 +1,8 @@ +export interface NFT { + address: string; + id: string; +} + +export interface NFTWithPrice extends NFT { + usd: number; +} \ No newline at end of file diff --git a/indexers/enterprise/src/chain/NetworkName.ts b/indexers/enterprise/src/chain/NetworkName.ts new file mode 100644 index 00000000..76765f11 --- /dev/null +++ b/indexers/enterprise/src/chain/NetworkName.ts @@ -0,0 +1 @@ +export type NetworkName = 'testnet' | 'mainnet' \ No newline at end of file diff --git a/indexers/enterprise/src/chain/fromChainAmount.ts b/indexers/enterprise/src/chain/fromChainAmount.ts new file mode 100644 index 00000000..a6fcfbba --- /dev/null +++ b/indexers/enterprise/src/chain/fromChainAmount.ts @@ -0,0 +1,5 @@ +import { Big } from "big.js"; + +export const fromChainAmount = (amount: string | number, decimals: number) => { + return Big(amount).div(Math.pow(10, decimals)).toNumber() +} \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getAssetBalance.ts b/indexers/enterprise/src/chain/getAssetBalance.ts new file mode 100644 index 00000000..49311ee2 --- /dev/null +++ b/indexers/enterprise/src/chain/getAssetBalance.ts @@ -0,0 +1,36 @@ +import { Asset } from "./Asset" +import { contractQuery, getBankBalance } from "./lcd" +import memoize from 'memoizee' + +interface GetAssetBalance { + asset: Asset + address: string +} + +interface CW20BalanceResponse { + balance?: string +} + +export const getAssetBalance = memoize(async ({ asset, address }: GetAssetBalance) => { + const { id, type } = asset + + if (type === 'native') { + const coins = await getBankBalance(address) + if (!coins) return '0' + + const coin = coins.get(asset.id); + + return coin?.amount?.toString() ?? '0' + } + + const { balance } = await contractQuery( + id, + { + balance: { + address, + }, + } + ); + + return balance ?? '0' +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getAssetInfo.ts b/indexers/enterprise/src/chain/getAssetInfo.ts new file mode 100644 index 00000000..d3fe4c29 --- /dev/null +++ b/indexers/enterprise/src/chain/getAssetInfo.ts @@ -0,0 +1,47 @@ +import { Asset, AssetInfo } from 'chain/Asset'; +import { getAssetsInfo } from './getAssetsInfo'; +import { contractQuery } from './lcd'; +import { NetworkName } from './NetworkName'; + +interface CW20TokenInfoResponse { + name: string; + symbol: string; + decimals: number; + total_supply: string; +} + +interface GetAssetInfoParams { + asset: Asset; + networkName: NetworkName +} + +export const getAssetInfo = async ({ asset: { id, type }, networkName }: GetAssetInfoParams): Promise => { + if (type === 'cw20') { + const { name, symbol, decimals } = await contractQuery(id, { + token_info: {}, + }); + + return { + name, + symbol, + decimals, + }; + } + + if (id === 'uluna') { + return { + name: 'LUNA', + symbol: 'LUNA', + icon: 'https://assets.terra.money/icon/svg/Luna.svg', + decimals: 6, + }; + } + + const assets = await getAssetsInfo(networkName); + const asset = assets.find((asset) => asset.id === id); + if (asset) { + return asset + } + + throw new Error(`Asset with id=${id} not found`); +}; diff --git a/indexers/enterprise/src/chain/getAssetPrice.ts b/indexers/enterprise/src/chain/getAssetPrice.ts new file mode 100644 index 00000000..54aa956e --- /dev/null +++ b/indexers/enterprise/src/chain/getAssetPrice.ts @@ -0,0 +1,8 @@ +import { Asset } from './Asset' +import { getPricesOfLiquidAssets } from './getPricesOfLiquidAssets' + +export const getAssetPrice = async (asset: Asset): Promise => { + const prices = await getPricesOfLiquidAssets() + + return prices[asset.id] || 0 +} \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getAssetsInfo.ts b/indexers/enterprise/src/chain/getAssetsInfo.ts new file mode 100644 index 00000000..36ad3ea1 --- /dev/null +++ b/indexers/enterprise/src/chain/getAssetsInfo.ts @@ -0,0 +1,50 @@ +import { NetworkName } from '@terra-money/apps/hooks'; +import { assertDefined } from '@terra-money/apps/utils'; +import axios from 'axios'; +import { AssetInfo } from 'chain/Asset'; +import { memoize } from 'utils/memoize'; + +const TFM_ASSETS_INFO_URL = 'https://api-terra2.tfm.com/tokens'; + +const TFL_ASSETS_INFO_URL = 'https://assets.terra.money/ibc/tokens.json' + +interface TFMAssetInfo { + contract_addr: string; + decimals: number; + name: string; + symbol: string; +} + +interface TFLAssetInfo { + denom: string; + sybmol?: string; + name: string; + icon: string; + decimals?: number; +} + +type TFLAssetsInfo = Record> + +export const getAssetsInfo = memoize(async (network: NetworkName = 'mainnet') => { + const { data: tflData } = await axios.get(TFL_ASSETS_INFO_URL); + const assets: Array = Object.values(tflData[network]).filter(asset => asset.decimals).map(info => ({ + name: info.name, + symbol: info.sybmol, + decimals: assertDefined(info.decimals), + icon: info.icon, + id: info.denom, + })) + + if (network === 'mainnet') { + const { data: tfmData } = await axios.get(TFM_ASSETS_INFO_URL); + const tfmAssets = tfmData.map(info => ({ + name: info.name, + symbol: info.symbol, + decimals: info.decimals, + id: info.contract_addr, + })) + assets.push(...tfmAssets) + } + + return assets; +}); diff --git a/indexers/enterprise/src/chain/getAssetsPrices.ts b/indexers/enterprise/src/chain/getAssetsPrices.ts new file mode 100644 index 00000000..12c6f25d --- /dev/null +++ b/indexers/enterprise/src/chain/getAssetsPrices.ts @@ -0,0 +1,19 @@ +import axios from 'axios' +import memoize from 'memoizee' + +const TFM_ASSETS_PRICES_URL = 'https://price.api.tfm.com/tokens/?limit=1500' + +type TFMChain = 'osmosis' | 'terra2' | 'juno' | 'terra_classic' + +interface TFMTokenInfo { + chain: TFMChain + usd: number +} + +type TFMResponse = Record + +export const getAssetsPrices = memoize(async () => { + const { data } = await axios.get(TFM_ASSETS_PRICES_URL) + + return data +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getFloorPricesOfSupportedNFTCollections.ts b/indexers/enterprise/src/chain/getFloorPricesOfSupportedNFTCollections.ts new file mode 100644 index 00000000..73ce94be --- /dev/null +++ b/indexers/enterprise/src/chain/getFloorPricesOfSupportedNFTCollections.ts @@ -0,0 +1,93 @@ +import memoize from 'memoizee' +import axios from 'axios' +import { getAssetPrice } from './getAssetPrice' +import { getAssetInfo } from './getAssetInfo' +import { fromChainAmount } from './fromChainAmount' + +const TFM_NFT_API = 'https://nft-terra2.tfm.dev/graphql' + +interface TFMError { + message: string +} + +interface TFMFloorPrice { + price?: number + denom?: string +} + +interface TFMStatisticContent { + floorPrice: TFMFloorPrice + collectionAddr: string +} + +interface TFMResponse { + data: { + collectionTable: { + content: TFMStatisticContent[] + } + }, + errors?: TFMError[] +} + +const query = ` +query MyQuery { + collectionTable(limit: 100000) { + content { + floorPrice + collectionAddr + } + } +} +` + +type NFTCollectionFloorPrice = Record + +const getDataFromTFMCollectionTable = memoize(async () => { + const { data: { data, errors } } = await axios.post(TFM_NFT_API, { + query, + operationName: "MyQuery", + variables: null + }) + + if (errors) { + throw new Error(`Failed to get NFT collections from ${TFM_NFT_API}: ${errors[0]?.message}`) + } + + return data.collectionTable.content +}) + +export const convertCollectionPriceToUsd = async ({ denom, price }: TFMFloorPrice) => { + if (!denom || !price) { + return 0 + } + + try { + const { decimals } = await getAssetInfo({ id: denom, type: 'native' }) + + const denomPrice = await getAssetPrice({ id: denom, type: 'native' }) + + return fromChainAmount(denomPrice, decimals) * price + } catch (err) { + console.error(`Error getting price of a denom=${denom}`, err) + } + + return 0 +} + +export const getFloorPricesOfSupportedNFTCollections = memoize(async () => { + const data = await getDataFromTFMCollectionTable() + + const record: NFTCollectionFloorPrice = {} + + await Promise.all(data.map(async ({ collectionAddr, floorPrice }) => { + record[collectionAddr] = await convertCollectionPriceToUsd(floorPrice) + })) + + return record +}) + +export const getSupportedNFTCollections = memoize(async () => { + const data = await getDataFromTFMCollectionTable() + + return new Set(data.map(({ collectionAddr }) => collectionAddr)) +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getNFTPrice.ts b/indexers/enterprise/src/chain/getNFTPrice.ts new file mode 100644 index 00000000..5963a666 --- /dev/null +++ b/indexers/enterprise/src/chain/getNFTPrice.ts @@ -0,0 +1,15 @@ +import { NFT } from "./NFT"; +import { getNFTCollectionFloorPrice } from "./getNftCollectionFloorPrice"; +import { getPricesOfNftsInCollection } from "./getPricesOfNftsInCollection"; + +export const getNFTPrice = async (nft: NFT): Promise => { + try { + const prices = await getPricesOfNftsInCollection(nft.address) + + return prices[nft.id] || 0 + } catch (err) { + console.error(`Error getting price for NFT collection=${nft.address} id=${nft.id}`, err.toString()) + } + + return getNFTCollectionFloorPrice(nft.address) +} \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getNftCollectionFloorPrice.ts b/indexers/enterprise/src/chain/getNftCollectionFloorPrice.ts new file mode 100644 index 00000000..f1cf13d2 --- /dev/null +++ b/indexers/enterprise/src/chain/getNftCollectionFloorPrice.ts @@ -0,0 +1,8 @@ +import memoize from 'memoizee' +import { getFloorPricesOfSupportedNFTCollections } from './getFloorPricesOfSupportedNFTCollections'; + +export const getNFTCollectionFloorPrice = memoize(async (address: string): Promise => { + const record = await getFloorPricesOfSupportedNFTCollections() + + return record[address] +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getNftIds.ts b/indexers/enterprise/src/chain/getNftIds.ts new file mode 100644 index 00000000..42f389bd --- /dev/null +++ b/indexers/enterprise/src/chain/getNftIds.ts @@ -0,0 +1,33 @@ +import { contractQuery } from "./lcd"; + +interface GetNftIdsParams { + collection: string + owner: string +} + +type NftIdsResponse = { + ids?: string[]; + tokens?: string[]; +}; + +export const getNftIds = async ({ collection, owner }: GetNftIdsParams) => { + const fetchNftIds = async (startAfter?: string): Promise => { + const response = await contractQuery(collection, { + tokens: { owner, start_after: startAfter }, + }); + + const ids = response.ids ?? response.tokens ?? [] + + if (!ids.length) { + return []; + } + + const lastId = ids[ids.length - 1]; + const nextIds = await fetchNftIds(lastId); + return [...(ids || []), ...nextIds]; + }; + + const ids = await fetchNftIds(); + + return ids +} \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getPricesOfLiquidAssets.ts b/indexers/enterprise/src/chain/getPricesOfLiquidAssets.ts new file mode 100644 index 00000000..180fbd2d --- /dev/null +++ b/indexers/enterprise/src/chain/getPricesOfLiquidAssets.ts @@ -0,0 +1,82 @@ +import memoize from 'memoizee' +import axios from 'axios' + +const TFM_PRICE_API = 'https://prod-juno.analytics.tfm.com/graphql' + +interface TFMError { + message: string +} + +interface TFMStatisticContent { + liquidity: string, + priceInvertedUsd: string + contractAddr: string + token0Addr: string + token1Addr: string +} + +interface TFMResponse { + data: { + statisticTableTokensList: { + content: TFMStatisticContent[] + } + }, + errors?: TFMError[] +} + +// docs: https://prod-juno.analytics.tfm.com/graphql# +const query = ` +query tokens_table($limit: Int, $skip: Int, $verifiedOnly: Boolean, $field: String, $asc: Boolean, $interval: String\u0021, $chain: String\u0021) { + statisticTableTokensList( + limit: $limit + skip: $skip + verifiedOnly: $verifiedOnly + field: $field + asc: $asc + interval: $interval + chain: $chain + ) { + content { + liquidity + priceInvertedUsd + contractAddr + token0Addr + token1Addr + } + pageInfo { + asc + count + limit + skip + sortField + } + } +} +` + +const MIN_LIQUIDITY = 5000 + +type AssetPrices = Record + +export const getPricesOfLiquidAssets = memoize(async () => { + const { data: { data, errors } } = await axios.post(TFM_PRICE_API, { + query, + operationName: "tokens_table", + variables: { limit: 500, skip: 0, verifiedOnly: false, field: "volume", asc: false, interval: "1d", chain: "terra2" } + }) + + if (errors) { + throw new Error(`Failed to get liquid asset prices from ${TFM_PRICE_API}: ${errors[0]?.message}`) + } + + + const record: AssetPrices = {} + + data.statisticTableTokensList.content.forEach(asset => { + if (Number(asset.liquidity) >= MIN_LIQUIDITY) { + record[asset.token0Addr] = Number(asset.priceInvertedUsd) + } + }) + + return record +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getPricesOfNftsInCollection.ts b/indexers/enterprise/src/chain/getPricesOfNftsInCollection.ts new file mode 100644 index 00000000..18c40f7d --- /dev/null +++ b/indexers/enterprise/src/chain/getPricesOfNftsInCollection.ts @@ -0,0 +1,62 @@ +import memoize from 'memoizee' +import axios from 'axios' +import { convertCollectionPriceToUsd } from './getFloorPricesOfSupportedNFTCollections' + +const TFM_NFT_API = 'https://nft-terra2.tfm.dev/graphql' + +interface TFMError { + message: string +} + +interface NftToken { + price: number | null + denom: string + collectionFloorPrice: number | null + tokenId: string +} + +interface TFMResponse { + data: { + token: { + tokens: NftToken[] + } + }, + errors?: TFMError[] +} + +const getQuery = (collection: string) => ` +query MyQuery { + token(collectionAddr: "${collection}", limit: 10000) { + tokens { + collectionFloorPrice + price + denom + tokenId + } + } +} +` + +type NftPrice = Record + +export const getPricesOfNftsInCollection = memoize(async (collection: string) => { + const { data: { data, errors } } = await axios.post(TFM_NFT_API, { + query: getQuery(collection), + operationName: "MyQuery", + }) + + if (errors) { + throw new Error(`Failed to get prices of NFTs in collection=${collection} from ${TFM_NFT_API}: ${errors[0]?.message}`) + } + + const record: NftPrice = {} + + await Promise.all(data.token.tokens.map(async (token) => { + const price = token.price || token.collectionFloorPrice + if (!price) return + + record[token.tokenId] = await convertCollectionPriceToUsd({ denom: token.denom, price }) + })) + + return record +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/getWhitelistedIBCTokens.ts b/indexers/enterprise/src/chain/getWhitelistedIBCTokens.ts new file mode 100644 index 00000000..7645881f --- /dev/null +++ b/indexers/enterprise/src/chain/getWhitelistedIBCTokens.ts @@ -0,0 +1,72 @@ +import { assertEnvVar } from "shared/assertEnvVar"; +import memoize from 'memoizee' +import axios from "axios"; + +export type TokenBase = { + key: string; + name: string; + symbol: string; + icon: string; + decimals: number; + coinGeckoId?: string; +}; + +export type IBCToken = TokenBase & { + type: "ibc"; + path: string; + base_denom: string; + denom: string; +}; + +export interface IBCTokensResponse { + [tokenAddr: string]: IBCToken; +} + +interface IBCTokensNetworkResponse { + [network: string]: IBCTokensResponse; +} + +export type CW20Token = TokenBase & { + type: "cw20"; + protocol: string; + token: string; +}; + +export interface CW20TokensResponse { + [tokenAddr: string]: CW20Token; +} + +const fixTokenResponse = < + T extends IBCTokensResponse +>( + type: "cw20" | "ibc", + tokens: T, + accessor: (key: string) => string = (k) => k +) => { + return Object.keys(tokens).reduce((prev, current) => { + const key = accessor(current); + return { + ...prev, + [key]: { + ...tokens[current], + type, + key, + // decimals are optional in the responses but its much easier for us not to worry + // about optionality within the app so we can standardize the default here + decimals: + tokens[current].decimals === undefined || + tokens[current].decimals === 0 + ? 6 + : tokens[current].decimals, + }, + }; + }, {}); +}; + +export const getWhitelistedIBCTokens = memoize(async () => { + const { data: tokens } = await axios.get('https://assets.terra.money/ibc/tokens.json'); + + const network = assertEnvVar('NETWORK') + + return tokens && tokens[network] ? fixTokenResponse('ibc', tokens[network], (key) => `ibc/${key}`) : {}; +}) \ No newline at end of file diff --git a/indexers/enterprise/src/chain/lcd.ts b/indexers/enterprise/src/chain/lcd.ts new file mode 100644 index 00000000..70a7ed43 --- /dev/null +++ b/indexers/enterprise/src/chain/lcd.ts @@ -0,0 +1,49 @@ +import { LCDClient } from "@terra-money/feather.js"; +import { assertEnvVar } from "shared/assertEnvVar"; +import memoize from 'memoizee' +import { sleep } from "shared/sleep"; + +export const getLCDClient = memoize((): LCDClient => { + const chainID = assertEnvVar('CHAIN_ID') + + return new LCDClient({ + [chainID]: { + chainID, + lcd: assertEnvVar('LCD_ENDPOINT'), + gasAdjustment: 1.75, + gasPrices: { uluna: 0.015 }, + prefix: 'terra' + } + }); +}); + +let hadTooManyRequestsErrorAt: undefined | number = undefined +const tooManyRequestsErrorCooldown = 1000 * 60 + +async function handleTooManyRequestsError(promise: Promise): Promise { + if (hadTooManyRequestsErrorAt && Date.now() - hadTooManyRequestsErrorAt < tooManyRequestsErrorCooldown) { + await sleep(tooManyRequestsErrorCooldown) + } + + try { + return await promise + } catch (err) { + if (err.response.status === 429) { + hadTooManyRequestsErrorAt = Date.now() + + return handleTooManyRequestsError(promise) + } else { + throw err + } + } +} + +export async function contractQuery(contractAddress: string, msg: object | string): Promise { + return handleTooManyRequestsError(getLCDClient().wasm.contractQuery(contractAddress, msg)) +} + +export const getBankBalance = memoize(async (address: string) => { + const promise = getLCDClient().bank.balance(address).then(([coins]) => coins) + + return handleTooManyRequestsError(promise) +}) \ No newline at end of file diff --git a/indexers/enterprise/src/treasury/getDaoAssets.ts b/indexers/enterprise/src/treasury/getDaoAssets.ts new file mode 100644 index 00000000..6ac0115d --- /dev/null +++ b/indexers/enterprise/src/treasury/getDaoAssets.ts @@ -0,0 +1,70 @@ +import { Asset, AssetWithInfoAndBalance, areSameAsset } from "chain/Asset"; +import { getAssetBalance } from "chain/getAssetBalance"; +import { contractQuery } from "chain/lcd"; +import { DaoEntity } from "indexers/daos/types"; +import { enterprise, enterprise_factory } from "types/contracts"; +import { withoutDuplicates } from "utils/withoutDuplicates"; +import { withoutUndefined } from "utils/withoutUndefined"; +import { getDaoTotalStakedAmount } from "./getDaoTotalStakedAmount"; +import Big from "big.js"; +import { getAssetInfo } from "chain/getAssetInfo"; +import { assertEnvVar } from "@apps-shared/indexers/utils/assertEnvVar"; +import { NetworkName } from "chain/NetworkName"; + +const toAsset = ( + response: enterprise.AssetInfoBaseFor_Addr | enterprise_factory.AssetInfoBaseFor_Addr +): Asset | undefined => { + if ('native' in response) { + return { + type: 'native', + id: response.native, + }; + } else if ('cw20' in response) { + return { + type: 'cw20', + id: response.cw20, + }; + } + + return undefined; +}; + +export const getDaoAssets = async ({ address, enterpriseFactoryContract, membershipContractAddress }: Pick) => { + const { assets: globalWhitelist } = await contractQuery(enterpriseFactoryContract, { global_asset_whitelist: {}, }); + const { assets: assetsWhitelist } = await contractQuery(address, { asset_whitelist: {}, }); + + const whitelist: Asset[] = withoutDuplicates(withoutUndefined([...globalWhitelist, ...assetsWhitelist].map(toAsset)), areSameAsset); + + const result: AssetWithInfoAndBalance[] = []; + await Promise.all(whitelist.map(async asset => { + let balance = '0' + try { + balance = await getAssetBalance({ asset, address }) + if (asset.id === membershipContractAddress) { + const totalStakedAmount = await getDaoTotalStakedAmount({ address }) + balance = Big(balance).minus(totalStakedAmount).toString() + } + if (Big(balance).lte(0)) return + } catch (err) { + console.error(`Failed to get balance of ${asset.type} asset with id=${asset.id}: ${err}`) + return + } + + try { + const info = await getAssetInfo({ + asset, + networkName: assertEnvVar('NETWORK') as NetworkName + }) + result.push({ + ...asset, + balance, + ...info + }) + } catch (err) { + console.error(`Failed to get asset info for ${asset.type} asset with id=${asset.id}: ${err}`) + return + } + })) + + return result +} \ No newline at end of file diff --git a/indexers/enterprise-stats/src/dao/getDaoTotalStakedAmount.ts b/indexers/enterprise/src/treasury/getDaoTotalStakedAmount.ts similarity index 85% rename from indexers/enterprise-stats/src/dao/getDaoTotalStakedAmount.ts rename to indexers/enterprise/src/treasury/getDaoTotalStakedAmount.ts index c60383fc..c89e7a7b 100644 --- a/indexers/enterprise-stats/src/dao/getDaoTotalStakedAmount.ts +++ b/indexers/enterprise/src/treasury/getDaoTotalStakedAmount.ts @@ -1,6 +1,6 @@ import { contractQuery } from "chain/lcd"; import { enterprise } from "types/contracts"; -import { Dao } from "./Dao"; +import { Dao } from "../../../enterprise-stats/src/dao/Dao"; export const getDaoTotalStakedAmount = async (dao: Pick) => { const { total_staked_amount } = await contractQuery( diff --git a/indexers/enterprise/src/utils/memoize.ts b/indexers/enterprise/src/utils/memoize.ts new file mode 100644 index 00000000..bad79d5d --- /dev/null +++ b/indexers/enterprise/src/utils/memoize.ts @@ -0,0 +1,23 @@ +export const memoize = any>( + func: T, + getKey?: (...args: any[]) => string +): T => { + const cache = new Map>() + + const memoizedFunc = (...args: Parameters) => { + const key = getKey ? getKey(...args) : JSON.stringify(args) + + const cachedResult = cache.get(key) + + if (!cachedResult) { + const result = func(...args) + cache.set(key, result) + + return result + } + + return cachedResult + } + + return memoizedFunc as T +} diff --git a/indexers/enterprise/src/utils/withoutDuplicates.ts b/indexers/enterprise/src/utils/withoutDuplicates.ts new file mode 100644 index 00000000..8a76d784 --- /dev/null +++ b/indexers/enterprise/src/utils/withoutDuplicates.ts @@ -0,0 +1,14 @@ +export function withoutDuplicates( + items: T[], + areEqual: (a: T, b: T) => boolean = (a, b) => a === b +): T[] { + const result: T[] = [] + + items.forEach((item) => { + if (!result.find((i) => areEqual(i, item))) { + result.push(item) + } + }) + + return result +} diff --git a/indexers/enterprise/src/utils/withoutUndefined.ts b/indexers/enterprise/src/utils/withoutUndefined.ts new file mode 100644 index 00000000..0508df9a --- /dev/null +++ b/indexers/enterprise/src/utils/withoutUndefined.ts @@ -0,0 +1,3 @@ +export function withoutUndefined(items: Array): T[] { + return items.filter((item) => item !== undefined) as T[] +} diff --git a/indexers/shared/utils/assertEnvVar.ts b/indexers/shared/utils/assertEnvVar.ts index c285b162..2ede597b 100644 --- a/indexers/shared/utils/assertEnvVar.ts +++ b/indexers/shared/utils/assertEnvVar.ts @@ -1,4 +1,4 @@ -type VariableName = "LCD_ENDPOINT" | "CHAIN_ID" +type VariableName = "LCD_ENDPOINT" | "CHAIN_ID" | "NETWORK" export const assertEnvVar = (name: VariableName): string => { const value = process.env[name]