| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- import {
- Bridge__factory,
- CHAIN_ID_ETH,
- CHAIN_ID_SOLANA,
- CHAIN_ID_TERRA,
- } from "@certusone/wormhole-sdk";
- import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
- import { Dispatch } from "@reduxjs/toolkit";
- import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
- import {
- AccountInfo,
- Connection,
- ParsedAccountData,
- PublicKey,
- } from "@solana/web3.js";
- import axios from "axios";
- import { formatUnits } from "ethers/lib/utils";
- import { useCallback, useEffect, useMemo, useState } from "react";
- import { useDispatch, useSelector } from "react-redux";
- import {
- Provider,
- useEthereumProvider,
- } from "../contexts/EthereumProviderContext";
- import { useSolanaWallet } from "../contexts/SolanaWalletContext";
- import {
- errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
- fetchSourceParsedTokenAccounts as fetchSourceParsedTokenAccountsNFT,
- NFTParsedTokenAccount,
- receiveSourceParsedTokenAccounts as receiveSourceParsedTokenAccountsNFT,
- setSourceParsedTokenAccount as setSourceParsedTokenAccountNFT,
- setSourceParsedTokenAccounts as setSourceParsedTokenAccountsNFT,
- setSourceWalletAddress as setSourceWalletAddressNFT,
- } from "../store/nftSlice";
- import {
- selectNFTSourceChain,
- selectNFTSourceParsedTokenAccounts,
- selectNFTSourceWalletAddress,
- selectSourceWalletAddress,
- selectTransferSourceChain,
- selectTransferSourceParsedTokenAccounts,
- } from "../store/selectors";
- import {
- errorSourceParsedTokenAccounts,
- fetchSourceParsedTokenAccounts,
- ParsedTokenAccount,
- receiveSourceParsedTokenAccounts,
- setAmount,
- setSourceParsedTokenAccount,
- setSourceParsedTokenAccounts,
- setSourceWalletAddress,
- } from "../store/transferSlice";
- import {
- COVALENT_GET_TOKENS_URL,
- ETH_TOKEN_BRIDGE_ADDRESS,
- SOLANA_HOST,
- WETH_ADDRESS,
- WETH_DECIMALS,
- } from "../utils/consts";
- import {
- extractMintAuthorityInfo,
- getMultipleAccountsRPC,
- } from "../utils/solana";
- export function createParsedTokenAccount(
- publicKey: string,
- mintKey: string,
- amount: string,
- decimals: number,
- uiAmount: number,
- uiAmountString: string,
- symbol?: string,
- name?: string,
- logo?: string,
- isNativeAsset?: boolean
- ): ParsedTokenAccount {
- return {
- publicKey: publicKey,
- mintKey: mintKey,
- amount,
- decimals,
- uiAmount,
- uiAmountString,
- symbol,
- name,
- logo,
- isNativeAsset,
- };
- }
- export function createNFTParsedTokenAccount(
- publicKey: string,
- mintKey: string,
- amount: string,
- decimals: number,
- uiAmount: number,
- uiAmountString: string,
- tokenId: string,
- symbol?: string,
- uri?: string,
- animation_url?: string,
- external_url?: string,
- image?: string,
- image_256?: string,
- name?: string
- ): NFTParsedTokenAccount {
- return {
- publicKey,
- mintKey,
- amount,
- decimals,
- uiAmount,
- uiAmountString,
- tokenId,
- uri,
- animation_url,
- external_url,
- image,
- image_256,
- symbol,
- name,
- };
- }
- const createParsedTokenAccountFromInfo = (
- pubkey: PublicKey,
- item: AccountInfo<ParsedAccountData>
- ): ParsedTokenAccount => {
- return {
- publicKey: pubkey?.toString(),
- mintKey: item.data.parsed?.info?.mint?.toString(),
- amount: item.data.parsed?.info?.tokenAmount?.amount,
- decimals: item.data.parsed?.info?.tokenAmount?.decimals,
- uiAmount: item.data.parsed?.info?.tokenAmount?.uiAmount,
- uiAmountString: item.data.parsed?.info?.tokenAmount?.uiAmountString,
- };
- };
- const createParsedTokenAccountFromCovalent = (
- walletAddress: string,
- covalent: CovalentData
- ): ParsedTokenAccount => {
- return {
- publicKey: walletAddress,
- mintKey: covalent.contract_address,
- amount: covalent.balance,
- decimals: covalent.contract_decimals,
- uiAmount: Number(formatUnits(covalent.balance, covalent.contract_decimals)),
- uiAmountString: formatUnits(covalent.balance, covalent.contract_decimals),
- symbol: covalent.contract_ticker_symbol,
- name: covalent.contract_name,
- logo: covalent.logo_url,
- };
- };
- const createNativeEthParsedTokenAccount = (
- provider: Provider,
- signerAddress: string | undefined
- ) => {
- return !(provider && signerAddress)
- ? Promise.reject()
- : provider.getBalance(signerAddress).then((balanceInWei) => {
- const balanceInEth = ethers.utils.formatEther(balanceInWei);
- return createParsedTokenAccount(
- signerAddress, //public key
- WETH_ADDRESS, //Mint key, On the other side this will be WETH, so this is hopefully a white lie.
- balanceInWei.toString(), //amount, in wei
- WETH_DECIMALS, //Luckily both ETH and WETH have 18 decimals, so this should not be an issue.
- parseFloat(balanceInEth), //This loses precision, but is a limitation of the current datamodel. This field is essentially deprecated
- balanceInEth.toString(), //This is the actual display field, which has full precision.
- "ETH", //A white lie for display purposes
- "Ethereum", //A white lie for display purposes
- undefined, //TODO logo
- true //isNativeAsset
- );
- });
- };
- const createNFTParsedTokenAccountFromCovalent = (
- walletAddress: string,
- covalent: CovalentData,
- nft_data: CovalentNFTData
- ): NFTParsedTokenAccount => {
- return {
- publicKey: walletAddress,
- mintKey: covalent.contract_address,
- amount: nft_data.token_balance,
- decimals: covalent.contract_decimals,
- uiAmount: Number(
- formatUnits(nft_data.token_balance, covalent.contract_decimals)
- ),
- uiAmountString: formatUnits(
- nft_data.token_balance,
- covalent.contract_decimals
- ),
- symbol: covalent.contract_ticker_symbol,
- logo: covalent.logo_url,
- tokenId: nft_data.token_id,
- uri: nft_data.token_url,
- animation_url: nft_data.external_data.animation_url,
- external_url: nft_data.external_data.external_url,
- image: nft_data.external_data.image,
- image_256: nft_data.external_data.image_256,
- name: nft_data.external_data.name,
- };
- };
- export type CovalentData = {
- contract_decimals: number;
- contract_ticker_symbol: string;
- contract_name: string;
- contract_address: string;
- logo_url: string | undefined;
- balance: string;
- quote: number | undefined;
- quote_rate: number | undefined;
- nft_data?: CovalentNFTData[];
- };
- export type CovalentNFTExternalData = {
- animation_url: string | null;
- external_url: string | null;
- image: string;
- image_256: string;
- name: string;
- };
- export type CovalentNFTData = {
- token_id: string;
- token_balance: string;
- external_data: CovalentNFTExternalData;
- token_url: string;
- };
- const getEthereumAccountsCovalent = async (
- walletAddress: string,
- nft?: boolean
- ): Promise<CovalentData[]> => {
- const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress, nft);
- try {
- const output = [] as CovalentData[];
- const response = await axios.get(url);
- const tokens = response.data.data.items;
- if (tokens instanceof Array && tokens.length) {
- for (const item of tokens) {
- // TODO: filter?
- if (
- item.contract_decimals !== undefined &&
- item.contract_ticker_symbol &&
- item.contract_address &&
- item.balance &&
- item.balance !== "0" &&
- (nft
- ? item.supports_erc?.includes("erc721")
- : item.supports_erc?.includes("erc20"))
- ) {
- output.push({ ...item } as CovalentData);
- }
- }
- }
- return output;
- } catch (error) {
- return Promise.reject("Unable to retrieve your Ethereum Tokens.");
- }
- };
- const getSolanaParsedTokenAccounts = (
- walletAddress: string,
- dispatch: Dispatch,
- nft: boolean
- ) => {
- const connection = new Connection(SOLANA_HOST, "finalized");
- dispatch(
- nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
- );
- return connection
- .getParsedTokenAccountsByOwner(new PublicKey(walletAddress), {
- programId: new PublicKey(TOKEN_PROGRAM_ID),
- })
- .then(
- (result) => {
- const mappedItems = result.value.map((item) =>
- createParsedTokenAccountFromInfo(item.pubkey, item.account)
- );
- dispatch(
- nft
- ? receiveSourceParsedTokenAccountsNFT(mappedItems)
- : receiveSourceParsedTokenAccounts(mappedItems)
- );
- },
- (error) => {
- dispatch(
- nft
- ? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata")
- : errorSourceParsedTokenAccounts("Failed to load token metadata.")
- );
- }
- );
- };
- /**
- * Fetches the balance of an asset for the connected wallet
- * This should handle every type of chain in the future, but only reads the Transfer state.
- */
- function useGetAvailableTokens(nft: boolean = false) {
- const dispatch = useDispatch();
- const tokenAccounts = useSelector(
- nft
- ? selectNFTSourceParsedTokenAccounts
- : selectTransferSourceParsedTokenAccounts
- );
- const lookupChain = useSelector(
- nft ? selectNFTSourceChain : selectTransferSourceChain
- );
- const solanaWallet = useSolanaWallet();
- const solPK = solanaWallet?.publicKey;
- const { provider, signer, signerAddress } = useEthereumProvider();
- const [covalent, setCovalent] = useState<any>(undefined);
- const [covalentLoading, setCovalentLoading] = useState(false);
- const [covalentError, setCovalentError] = useState<string | undefined>(
- undefined
- );
- const [ethNativeAccount, setEthNativeAccount] = useState<any>(undefined);
- const [ethNativeAccountLoading, setEthNativeAccountLoading] = useState(false);
- const [ethNativeAccountError, setEthNativeAccountError] = useState<
- string | undefined
- >(undefined);
- const [solanaMintAccounts, setSolanaMintAccounts] = useState<any>(undefined);
- const [solanaMintAccountsLoading, setSolanaMintAccountsLoading] =
- useState(false);
- const [solanaMintAccountsError, setSolanaMintAccountsError] = useState<
- string | undefined
- >(undefined);
- const selectedSourceWalletAddress = useSelector(
- nft ? selectNFTSourceWalletAddress : selectSourceWalletAddress
- );
- const currentSourceWalletAddress: string | undefined =
- lookupChain === CHAIN_ID_ETH
- ? signerAddress
- : lookupChain === CHAIN_ID_SOLANA
- ? solPK?.toString()
- : undefined;
- const resetSourceAccounts = useCallback(() => {
- dispatch(
- nft
- ? setSourceWalletAddressNFT(undefined)
- : setSourceWalletAddress(undefined)
- );
- dispatch(
- nft
- ? setSourceParsedTokenAccountNFT(undefined)
- : setSourceParsedTokenAccount(undefined)
- );
- dispatch(
- nft
- ? setSourceParsedTokenAccountsNFT(undefined)
- : setSourceParsedTokenAccounts(undefined)
- );
- !nft && dispatch(setAmount(""));
- setCovalent(undefined); //These need to be included in the reset because they have balances on them.
- setCovalentLoading(false);
- setCovalentError("");
- setEthNativeAccount(undefined);
- setEthNativeAccountLoading(false);
- setEthNativeAccountError("");
- }, [setCovalent, dispatch, nft]);
- //TODO this useEffect could be somewhere else in the codebase
- //It resets the SourceParsedTokens accounts when the wallet changes
- useEffect(() => {
- if (
- selectedSourceWalletAddress !== undefined &&
- currentSourceWalletAddress !== undefined &&
- currentSourceWalletAddress !== selectedSourceWalletAddress
- ) {
- resetSourceAccounts();
- return;
- } else {
- }
- }, [
- selectedSourceWalletAddress,
- currentSourceWalletAddress,
- dispatch,
- resetSourceAccounts,
- ]);
- //Solana accountinfos load
- useEffect(() => {
- if (lookupChain === CHAIN_ID_SOLANA && solPK) {
- if (
- !(tokenAccounts.data || tokenAccounts.isFetching || tokenAccounts.error)
- ) {
- getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
- }
- }
- return () => {};
- }, [dispatch, solanaWallet, lookupChain, solPK, tokenAccounts, nft]);
- //Solana Mint Accounts lookup
- useEffect(() => {
- if (lookupChain !== CHAIN_ID_SOLANA || !tokenAccounts.data?.length) {
- return () => {};
- }
- let cancelled = false;
- setSolanaMintAccountsLoading(true);
- setSolanaMintAccountsError(undefined);
- const mintAddresses = tokenAccounts.data.map((x) => x.mintKey);
- //This is a known wormhole v1 token on testnet
- // mintAddresses.push("4QixXecTZ4zdZGa39KH8gVND5NZ2xcaB12wiBhE4S7rn");
- //SOLT devnet token
- // mintAddresses.push("2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ");
- const connection = new Connection(SOLANA_HOST, "finalized");
- getMultipleAccountsRPC(
- connection,
- mintAddresses.map((x) => new PublicKey(x))
- ).then(
- (results) => {
- if (!cancelled) {
- const output = new Map<String, string | null>();
- results.forEach((result, index) =>
- output.set(
- mintAddresses[index],
- (result && extractMintAuthorityInfo(result)) || null
- )
- );
- setSolanaMintAccounts(output);
- setSolanaMintAccountsLoading(false);
- }
- },
- (error) => {
- if (!cancelled) {
- setSolanaMintAccounts(undefined);
- setSolanaMintAccountsLoading(false);
- setSolanaMintAccountsError(
- "Could not retrieve Solana mint accounts."
- );
- }
- }
- );
- return () => (cancelled = true);
- }, [tokenAccounts.data, lookupChain]);
- //Ethereum native asset load
- useEffect(() => {
- let cancelled = false;
- if (
- signerAddress &&
- lookupChain === CHAIN_ID_ETH &&
- !ethNativeAccount &&
- !nft
- ) {
- setEthNativeAccountLoading(true);
- createNativeEthParsedTokenAccount(provider, signerAddress).then(
- (result) => {
- console.log("create native account returned with value", result);
- if (!cancelled) {
- setEthNativeAccount(result);
- setEthNativeAccountLoading(false);
- setEthNativeAccountError("");
- }
- },
- (error) => {
- if (!cancelled) {
- setEthNativeAccount(undefined);
- setEthNativeAccountLoading(false);
- setEthNativeAccountError("Unable to retrieve your ETH balance.");
- }
- }
- );
- }
- return () => {
- cancelled = true;
- };
- }, [lookupChain, provider, signerAddress, nft, ethNativeAccount]);
- //Ethereum covalent accounts load
- useEffect(() => {
- //const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
- // const nftTestWallet1 = "0x3f304c6721f35ff9af00fd32650c8e0a982180ab";
- // const nftTestWallet2 = "0x98ed231428088eb440e8edb5cc8d66dcf913b86e";
- let cancelled = false;
- const walletAddress = signerAddress;
- if (walletAddress && lookupChain === CHAIN_ID_ETH && !covalent) {
- //TODO less cancel
- !cancelled && setCovalentLoading(true);
- !cancelled &&
- dispatch(
- nft
- ? fetchSourceParsedTokenAccountsNFT()
- : fetchSourceParsedTokenAccounts()
- );
- getEthereumAccountsCovalent(walletAddress, nft).then(
- (accounts) => {
- !cancelled && setCovalentLoading(false);
- !cancelled && setCovalentError(undefined);
- !cancelled && setCovalent(accounts);
- !cancelled &&
- dispatch(
- nft
- ? receiveSourceParsedTokenAccountsNFT(
- accounts.reduce((arr, current) => {
- if (current.nft_data) {
- current.nft_data.forEach((x) =>
- arr.push(
- createNFTParsedTokenAccountFromCovalent(
- walletAddress,
- current,
- x
- )
- )
- );
- }
- return arr;
- }, [] as NFTParsedTokenAccount[])
- )
- : receiveSourceParsedTokenAccounts(
- accounts.map((x) =>
- createParsedTokenAccountFromCovalent(walletAddress, x)
- )
- )
- );
- },
- () => {
- !cancelled &&
- dispatch(
- nft
- ? errorSourceParsedTokenAccountsNFT(
- "Cannot load your Ethereum NFTs at the moment."
- )
- : errorSourceParsedTokenAccounts(
- "Cannot load your Ethereum tokens at the moment."
- )
- );
- !cancelled &&
- setCovalentError("Cannot load your Ethereum tokens at the moment.");
- !cancelled && setCovalentLoading(false);
- }
- );
- return () => {
- cancelled = true;
- };
- }
- }, [lookupChain, provider, signerAddress, dispatch, nft, covalent]);
- //Terra accounts load
- //At present, we don't have any mechanism for doing this.
- useEffect(() => {}, []);
- const ethAccounts = useMemo(() => {
- const output = { ...tokenAccounts };
- output.data = output.data?.slice() || [];
- output.isFetching = output.isFetching || ethNativeAccountLoading;
- output.error = output.error || ethNativeAccountError;
- ethNativeAccount && output.data && output.data.unshift(ethNativeAccount);
- return output;
- }, [
- ethNativeAccount,
- ethNativeAccountLoading,
- ethNativeAccountError,
- tokenAccounts,
- ]);
- return lookupChain === CHAIN_ID_SOLANA
- ? {
- tokenAccounts: tokenAccounts,
- mintAccounts: {
- data: solanaMintAccounts,
- isFetching: solanaMintAccountsLoading,
- error: solanaMintAccountsError,
- receivedAt: null, //TODO
- },
- resetAccounts: resetSourceAccounts,
- }
- : lookupChain === CHAIN_ID_ETH
- ? {
- tokenAccounts: ethAccounts,
- covalent: {
- data: covalent,
- isFetching: covalentLoading,
- error: covalentError,
- receivedAt: null, //TODO
- },
- resetAccounts: resetSourceAccounts,
- }
- : lookupChain === CHAIN_ID_TERRA
- ? {
- resetAccounts: resetSourceAccounts,
- }
- : undefined;
- }
- export default useGetAvailableTokens;
|