From 93a09e01aac396c3a083b5ca543e33bbccceb232 Mon Sep 17 00:00:00 2001 From: Chase Moran Date: Fri, 29 Oct 2021 07:39:14 -0400 Subject: [PATCH] bridge_ui: solana price estimates in tvl Change-Id: I041b36dc7efe1bb7e20376aace4e5181d6c35069 --- bridge_ui/package-lock.json | 188 +++++++++++++++++++++++- bridge_ui/package.json | 1 + bridge_ui/src/hooks/useTVL.ts | 104 ++++++++++++- bridge_ui/src/utils/SolanaPriceStore.ts | 81 ++++++++++ 4 files changed, 366 insertions(+), 8 deletions(-) create mode 100644 bridge_ui/src/utils/SolanaPriceStore.ts diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index 036eba65..a089d236 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -13,6 +13,7 @@ "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", "@metamask/detect-provider": "^1.2.0", + "@project-serum/serum": "^0.13.60", "@reduxjs/toolkit": "^1.6.1", "@solana/spl-token": "^0.1.6", "@solana/spl-token-registry": "^0.2.216", @@ -62,7 +63,7 @@ }, "../sdk/js": { "name": "@certusone/wormhole-sdk", - "version": "0.0.5", + "version": "0.0.8", "license": "Apache-2.0", "dependencies": { "@improbable-eng/grpc-web": "^0.14.0", @@ -5431,6 +5432,82 @@ "node": ">= 8" } }, + "node_modules/@project-serum/anchor": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz", + "integrity": "sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==", + "dependencies": { + "@project-serum/borsh": "^0.2.2", + "@solana/web3.js": "^1.17.0", + "base64-js": "^1.5.1", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.0", + "camelcase": "^5.3.1", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "find": "^0.3.0", + "js-sha256": "^0.9.0", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=11" + } + }, + "node_modules/@project-serum/anchor/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@project-serum/anchor/node_modules/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, + "node_modules/@project-serum/anchor/node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/@project-serum/borsh": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.2.tgz", + "integrity": "sha512-Ms+aWmGVW6bWd3b0+MWwoaYig2QD0F90h0uhr7AzY3dpCb5e2S6RsRW02vFTfa085pY2VLB7nTZNbFECQ1liTg==", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.2.0" + } + }, + "node_modules/@project-serum/serum": { + "version": "0.13.60", + "resolved": "https://registry.npmjs.org/@project-serum/serum/-/serum-0.13.60.tgz", + "integrity": "sha512-fGsp9F0ZAS48YQ2HNy+6CNoifJESFXxVsOLPd9QK1XNV8CTuQoECOnVXxV6s5cKGre8pLNq5hrhi5J6aCGauEQ==", + "dependencies": { + "@project-serum/anchor": "^0.11.1", + "@solana/spl-token": "^0.1.6", + "@solana/web3.js": "^1.21.0", + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@project-serum/sol-wallet-adapter": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.2.5.tgz", @@ -18816,6 +18893,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/find": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", + "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", + "dependencies": { + "traverse-chain": "~0.1.0" + } + }, "node_modules/find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -26199,6 +26284,11 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.1.tgz", "integrity": "sha512-XyYXEUTP3ykPPnGPoesMr4yBygopit99iXW52yT1EWrkzwzvtAor/pbf+EBuDkwqSty7K10LeTjCkUn8c166aQ==" }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -39823,6 +39913,11 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -39855,6 +39950,11 @@ "node": ">=8" } }, + "node_modules/traverse-chain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=" + }, "node_modules/trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", @@ -48005,6 +48105,69 @@ } } }, + "@project-serum/anchor": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz", + "integrity": "sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==", + "requires": { + "@project-serum/borsh": "^0.2.2", + "@solana/web3.js": "^1.17.0", + "base64-js": "^1.5.1", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.0", + "camelcase": "^5.3.1", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "find": "^0.3.0", + "js-sha256": "^0.9.0", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "toml": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, + "snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + } + } + }, + "@project-serum/borsh": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.2.tgz", + "integrity": "sha512-Ms+aWmGVW6bWd3b0+MWwoaYig2QD0F90h0uhr7AzY3dpCb5e2S6RsRW02vFTfa085pY2VLB7nTZNbFECQ1liTg==", + "requires": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + } + }, + "@project-serum/serum": { + "version": "0.13.60", + "resolved": "https://registry.npmjs.org/@project-serum/serum/-/serum-0.13.60.tgz", + "integrity": "sha512-fGsp9F0ZAS48YQ2HNy+6CNoifJESFXxVsOLPd9QK1XNV8CTuQoECOnVXxV6s5cKGre8pLNq5hrhi5J6aCGauEQ==", + "requires": { + "@project-serum/anchor": "^0.11.1", + "@solana/spl-token": "^0.1.6", + "@solana/web3.js": "^1.21.0", + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + } + }, "@project-serum/sol-wallet-adapter": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.2.5.tgz", @@ -59297,6 +59460,14 @@ } } }, + "find": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", + "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", + "requires": { + "traverse-chain": "~0.1.0" + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -65073,6 +65244,11 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.1.tgz", "integrity": "sha512-XyYXEUTP3ykPPnGPoesMr4yBygopit99iXW52yT1EWrkzwzvtAor/pbf+EBuDkwqSty7K10LeTjCkUn8c166aQ==" }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -76353,6 +76529,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -76378,6 +76559,11 @@ "punycode": "^2.1.1" } }, + "traverse-chain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=" + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", diff --git a/bridge_ui/package.json b/bridge_ui/package.json index 313eda60..6016b4cb 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -8,6 +8,7 @@ "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", "@metamask/detect-provider": "^1.2.0", + "@project-serum/serum": "^0.13.60", "@reduxjs/toolkit": "^1.6.1", "@solana/spl-token": "^0.1.6", "@solana/spl-token-registry": "^0.2.216", diff --git a/bridge_ui/src/hooks/useTVL.ts b/bridge_ui/src/hooks/useTVL.ts index 2993a150..b9719656 100644 --- a/bridge_ui/src/hooks/useTVL.ts +++ b/bridge_ui/src/hooks/useTVL.ts @@ -7,6 +7,7 @@ import { } from "@certusone/wormhole-sdk"; import { formatUnits } from "@ethersproject/units"; import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { TokenInfo } from "@solana/spl-token-registry"; import { AccountInfo, Connection, @@ -26,12 +27,14 @@ import { TERRA_SWAPRATE_URL, TERRA_TOKEN_BRIDGE_ADDRESS, } from "../utils/consts"; +import { priceStore, serumMarkets } from "../utils/SolanaPriceStore"; import { formatNativeDenom, getNativeTerraIcon, NATIVE_TERRA_DECIMALS, } from "../utils/terra"; import useMetadata, { GenericMetadata } from "./useMetadata"; +import useSolanaTokenMap from "./useSolanaTokenMap"; import useTerraNativeBalances from "./useTerraNativeBalances"; export type TVL = { @@ -74,7 +77,8 @@ const calcSolanaTVL = ( accounts: | { pubkey: PublicKey; account: AccountInfo }[] | undefined, - metaData: DataWrapper> + metaData: DataWrapper>, + solanaPrices: DataWrapper> ) => { const output: TVL[] = []; if ( @@ -82,7 +86,9 @@ const calcSolanaTVL = ( !accounts.length || metaData.isFetching || metaData.error || - !metaData.data + !metaData.data || + solanaPrices.isFetching || + !solanaPrices.data ) { return output; } @@ -91,14 +97,20 @@ const calcSolanaTVL = ( const genericMetadata = metaData.data?.get( item.account.data.parsed?.info?.mint?.toString() ); + const mint = item.account.data.parsed?.info?.mint?.toString(); + const price = solanaPrices?.data?.get(mint); output.push({ logo: genericMetadata?.logo || undefined, symbol: genericMetadata?.symbol || undefined, name: genericMetadata?.tokenName || undefined, amount: item.account.data.parsed?.info?.tokenAmount?.uiAmount || "0", //Should always be defined. - totalValue: undefined, - quotePrice: undefined, - assetAddress: item.account.data.parsed?.info?.mint?.toString(), + totalValue: price + ? parseFloat( + item.account.data.parsed?.info?.tokenAmount?.uiAmount || "0" + ) * price + : undefined, + quotePrice: price, + assetAddress: mint, originChainId: CHAIN_ID_SOLANA, originChain: "Solana", }); @@ -175,6 +187,82 @@ const useTerraTVL = () => { ); }; +const useSolanaPrices = ( + mintAddresses: string[], + tokenMap: DataWrapper +) => { + const [isLoading, setIsLoading] = useState(false); + const [priceMap, setPriceMap] = useState | null>(null); + const [error] = useState(""); + + useEffect(() => { + let cancelled = false; + + if (!mintAddresses || !mintAddresses.length || !tokenMap.data) { + return; + } + + const relevantMarkets: { + publicKey?: PublicKey; + name: string; + deprecated?: boolean; + mintAddress: string; + }[] = []; + mintAddresses.forEach((address) => { + const tokenInfo = tokenMap.data?.find((x) => x.address === address); + const relevantMarket = tokenInfo && serumMarkets[tokenInfo.symbol]; + if (relevantMarket) { + relevantMarkets.push({ ...relevantMarket, mintAddress: address }); + } + }); + + setIsLoading(true); + const priceMap: Map = new Map(); + const connection = new Connection(SOLANA_HOST); + const promises: Promise[] = []; + //Load all the revelevant markets into the priceMap + relevantMarkets.forEach((market) => { + const marketName: string = market.name; + promises.push( + priceStore + .getPrice(connection, marketName) + .then((result) => { + priceMap.set(market.mintAddress, result); + }) + .catch((e) => { + //Do nothing, we just won't load this price. + return Promise.resolve(); + }) + ); + }); + + Promise.all(promises).then(() => { + //By this point all the relevant markets are loaded. + if (!cancelled) { + setPriceMap(priceMap); + setIsLoading(false); + } + }); + + return () => { + cancelled = true; + return; + }; + }, [mintAddresses, tokenMap.data]); + + return useMemo(() => { + return { + isFetching: isLoading, + data: priceMap || null, + error: error, + receivedAt: null, + }; + }, [error, priceMap, isLoading]); +}; + const useTVL = (): DataWrapper => { const [ethCovalentData, setEthCovalentData] = useState(undefined); const [ethCovalentIsLoading, setEthCovalentIsLoading] = useState(false); @@ -202,12 +290,14 @@ const useTVL = (): DataWrapper => { }, [solanaCustodyTokens]); const solanaMetadata = useMetadata(CHAIN_ID_SOLANA, mintAddresses); + const solanaTokenMap = useSolanaTokenMap(); + const solanaPrices = useSolanaPrices(mintAddresses, solanaTokenMap); const { isLoading: isTerraLoading, terraTVL } = useTerraTVL(); const solanaTVL = useMemo( - () => calcSolanaTVL(solanaCustodyTokens, solanaMetadata), - [solanaCustodyTokens, solanaMetadata] + () => calcSolanaTVL(solanaCustodyTokens, solanaMetadata, solanaPrices), + [solanaCustodyTokens, solanaMetadata, solanaPrices] ); const ethTVL = useMemo( () => calcEvmTVL(ethCovalentData, CHAIN_ID_ETH), diff --git a/bridge_ui/src/utils/SolanaPriceStore.ts b/bridge_ui/src/utils/SolanaPriceStore.ts new file mode 100644 index 00000000..2f17549d --- /dev/null +++ b/bridge_ui/src/utils/SolanaPriceStore.ts @@ -0,0 +1,81 @@ +import { MARKETS } from "@project-serum/serum"; +import { Connection, PublicKey } from "@solana/web3.js"; + +export interface Markets { + [coin: string]: { + publicKey?: PublicKey; + name: string; + deprecated?: boolean; + }; +} + +export const serumMarkets = (() => { + const m: Markets = {}; + MARKETS.forEach((market) => { + const coin = market.name.split("/")[0]; + if (m[coin]) { + // Only override a market if it's not deprecated . + if (!m.deprecated) { + m[coin] = { + publicKey: market.address, + name: market.name.split("/").join(""), + }; + } + } else { + m[coin] = { + publicKey: market.address, + name: market.name.split("/").join(""), + }; + } + }); + + m["USDC"] = m["USDT"]; + + return m; +})(); + +// Create a cached API wrapper to avoid rate limits. +class PriceStore { + cache: Map; + + constructor() { + this.cache = new Map(); + } + + async getPrice( + connection: Connection, + marketName: string + ): Promise { + return new Promise((resolve, reject) => { + if (this.cache.get(marketName) === undefined) { + fetch(`https://serum-api.bonfida.com/orderbooks/${marketName}`).then( + (resp) => { + resp.json().then((resp) => { + if (resp.data.asks === null || resp.data.bids === null) { + resolve(undefined); + } else if ( + resp.data.asks.length === 0 && + resp.data.bids.length === 0 + ) { + resolve(undefined); + } else if (resp.data.asks.length === 0) { + resolve(resp.data.bids[0].price); + } else if (resp.data.bids.length === 0) { + resolve(resp.data.asks[0].price); + } else { + const mid = + (resp.data.asks[0].price + resp.data.bids[0].price) / 2.0; + this.cache.set(marketName, mid); + resolve(this.cache.get(marketName)); + } + }); + } + ); + } else { + return resolve(this.cache.get(marketName)); + } + }); + } +} + +export const priceStore = new PriceStore();