bridge_ui: solana price estimates in tvl

Change-Id: I041b36dc7efe1bb7e20376aace4e5181d6c35069
This commit is contained in:
Chase Moran 2021-10-29 07:39:14 -04:00 committed by Evan Gray
parent 1a99ea1f85
commit 93a09e01aa
4 changed files with 366 additions and 8 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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<ParsedAccountData> }[]
| undefined,
metaData: DataWrapper<Map<string, GenericMetadata>>
metaData: DataWrapper<Map<string, GenericMetadata>>,
solanaPrices: DataWrapper<Map<string, number | undefined>>
) => {
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<TokenInfo[]>
) => {
const [isLoading, setIsLoading] = useState(false);
const [priceMap, setPriceMap] = useState<Map<
string,
number | undefined
> | 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<string, number | undefined> = new Map();
const connection = new Connection(SOLANA_HOST);
const promises: Promise<void>[] = [];
//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<TVL[]> => {
const [ethCovalentData, setEthCovalentData] = useState(undefined);
const [ethCovalentIsLoading, setEthCovalentIsLoading] = useState(false);
@ -202,12 +290,14 @@ const useTVL = (): DataWrapper<TVL[]> => {
}, [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),

View File

@ -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<String, number | undefined>;
constructor() {
this.cache = new Map();
}
async getPrice(
connection: Connection,
marketName: string
): Promise<number | undefined> {
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();