useGetSourceParsedTokenAccounts.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import {
  2. Bridge__factory,
  3. CHAIN_ID_ETH,
  4. CHAIN_ID_SOLANA,
  5. CHAIN_ID_TERRA,
  6. } from "@certusone/wormhole-sdk";
  7. import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
  8. import { Dispatch } from "@reduxjs/toolkit";
  9. import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
  10. import {
  11. AccountInfo,
  12. Connection,
  13. ParsedAccountData,
  14. PublicKey,
  15. } from "@solana/web3.js";
  16. import axios from "axios";
  17. import { formatUnits } from "ethers/lib/utils";
  18. import { useCallback, useEffect, useMemo, useState } from "react";
  19. import { useDispatch, useSelector } from "react-redux";
  20. import {
  21. Provider,
  22. useEthereumProvider,
  23. } from "../contexts/EthereumProviderContext";
  24. import { useSolanaWallet } from "../contexts/SolanaWalletContext";
  25. import {
  26. errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
  27. fetchSourceParsedTokenAccounts as fetchSourceParsedTokenAccountsNFT,
  28. NFTParsedTokenAccount,
  29. receiveSourceParsedTokenAccounts as receiveSourceParsedTokenAccountsNFT,
  30. setSourceParsedTokenAccount as setSourceParsedTokenAccountNFT,
  31. setSourceParsedTokenAccounts as setSourceParsedTokenAccountsNFT,
  32. setSourceWalletAddress as setSourceWalletAddressNFT,
  33. } from "../store/nftSlice";
  34. import {
  35. selectNFTSourceChain,
  36. selectNFTSourceParsedTokenAccounts,
  37. selectNFTSourceWalletAddress,
  38. selectSourceWalletAddress,
  39. selectTransferSourceChain,
  40. selectTransferSourceParsedTokenAccounts,
  41. } from "../store/selectors";
  42. import {
  43. errorSourceParsedTokenAccounts,
  44. fetchSourceParsedTokenAccounts,
  45. ParsedTokenAccount,
  46. receiveSourceParsedTokenAccounts,
  47. setAmount,
  48. setSourceParsedTokenAccount,
  49. setSourceParsedTokenAccounts,
  50. setSourceWalletAddress,
  51. } from "../store/transferSlice";
  52. import {
  53. COVALENT_GET_TOKENS_URL,
  54. ETH_TOKEN_BRIDGE_ADDRESS,
  55. SOLANA_HOST,
  56. WETH_ADDRESS,
  57. WETH_DECIMALS,
  58. } from "../utils/consts";
  59. import {
  60. extractMintAuthorityInfo,
  61. getMultipleAccountsRPC,
  62. } from "../utils/solana";
  63. export function createParsedTokenAccount(
  64. publicKey: string,
  65. mintKey: string,
  66. amount: string,
  67. decimals: number,
  68. uiAmount: number,
  69. uiAmountString: string,
  70. symbol?: string,
  71. name?: string,
  72. logo?: string,
  73. isNativeAsset?: boolean
  74. ): ParsedTokenAccount {
  75. return {
  76. publicKey: publicKey,
  77. mintKey: mintKey,
  78. amount,
  79. decimals,
  80. uiAmount,
  81. uiAmountString,
  82. symbol,
  83. name,
  84. logo,
  85. isNativeAsset,
  86. };
  87. }
  88. export function createNFTParsedTokenAccount(
  89. publicKey: string,
  90. mintKey: string,
  91. amount: string,
  92. decimals: number,
  93. uiAmount: number,
  94. uiAmountString: string,
  95. tokenId: string,
  96. symbol?: string,
  97. uri?: string,
  98. animation_url?: string,
  99. external_url?: string,
  100. image?: string,
  101. image_256?: string,
  102. name?: string
  103. ): NFTParsedTokenAccount {
  104. return {
  105. publicKey,
  106. mintKey,
  107. amount,
  108. decimals,
  109. uiAmount,
  110. uiAmountString,
  111. tokenId,
  112. uri,
  113. animation_url,
  114. external_url,
  115. image,
  116. image_256,
  117. symbol,
  118. name,
  119. };
  120. }
  121. const createParsedTokenAccountFromInfo = (
  122. pubkey: PublicKey,
  123. item: AccountInfo<ParsedAccountData>
  124. ): ParsedTokenAccount => {
  125. return {
  126. publicKey: pubkey?.toString(),
  127. mintKey: item.data.parsed?.info?.mint?.toString(),
  128. amount: item.data.parsed?.info?.tokenAmount?.amount,
  129. decimals: item.data.parsed?.info?.tokenAmount?.decimals,
  130. uiAmount: item.data.parsed?.info?.tokenAmount?.uiAmount,
  131. uiAmountString: item.data.parsed?.info?.tokenAmount?.uiAmountString,
  132. };
  133. };
  134. const createParsedTokenAccountFromCovalent = (
  135. walletAddress: string,
  136. covalent: CovalentData
  137. ): ParsedTokenAccount => {
  138. return {
  139. publicKey: walletAddress,
  140. mintKey: covalent.contract_address,
  141. amount: covalent.balance,
  142. decimals: covalent.contract_decimals,
  143. uiAmount: Number(formatUnits(covalent.balance, covalent.contract_decimals)),
  144. uiAmountString: formatUnits(covalent.balance, covalent.contract_decimals),
  145. symbol: covalent.contract_ticker_symbol,
  146. name: covalent.contract_name,
  147. logo: covalent.logo_url,
  148. };
  149. };
  150. const createNativeEthParsedTokenAccount = (
  151. provider: Provider,
  152. signerAddress: string | undefined
  153. ) => {
  154. return !(provider && signerAddress)
  155. ? Promise.reject()
  156. : provider.getBalance(signerAddress).then((balanceInWei) => {
  157. const balanceInEth = ethers.utils.formatEther(balanceInWei);
  158. return createParsedTokenAccount(
  159. signerAddress, //public key
  160. WETH_ADDRESS, //Mint key, On the other side this will be WETH, so this is hopefully a white lie.
  161. balanceInWei.toString(), //amount, in wei
  162. WETH_DECIMALS, //Luckily both ETH and WETH have 18 decimals, so this should not be an issue.
  163. parseFloat(balanceInEth), //This loses precision, but is a limitation of the current datamodel. This field is essentially deprecated
  164. balanceInEth.toString(), //This is the actual display field, which has full precision.
  165. "ETH", //A white lie for display purposes
  166. "Ethereum", //A white lie for display purposes
  167. undefined, //TODO logo
  168. true //isNativeAsset
  169. );
  170. });
  171. };
  172. const createNFTParsedTokenAccountFromCovalent = (
  173. walletAddress: string,
  174. covalent: CovalentData,
  175. nft_data: CovalentNFTData
  176. ): NFTParsedTokenAccount => {
  177. return {
  178. publicKey: walletAddress,
  179. mintKey: covalent.contract_address,
  180. amount: nft_data.token_balance,
  181. decimals: covalent.contract_decimals,
  182. uiAmount: Number(
  183. formatUnits(nft_data.token_balance, covalent.contract_decimals)
  184. ),
  185. uiAmountString: formatUnits(
  186. nft_data.token_balance,
  187. covalent.contract_decimals
  188. ),
  189. symbol: covalent.contract_ticker_symbol,
  190. logo: covalent.logo_url,
  191. tokenId: nft_data.token_id,
  192. uri: nft_data.token_url,
  193. animation_url: nft_data.external_data.animation_url,
  194. external_url: nft_data.external_data.external_url,
  195. image: nft_data.external_data.image,
  196. image_256: nft_data.external_data.image_256,
  197. name: nft_data.external_data.name,
  198. };
  199. };
  200. export type CovalentData = {
  201. contract_decimals: number;
  202. contract_ticker_symbol: string;
  203. contract_name: string;
  204. contract_address: string;
  205. logo_url: string | undefined;
  206. balance: string;
  207. quote: number | undefined;
  208. quote_rate: number | undefined;
  209. nft_data?: CovalentNFTData[];
  210. };
  211. export type CovalentNFTExternalData = {
  212. animation_url: string | null;
  213. external_url: string | null;
  214. image: string;
  215. image_256: string;
  216. name: string;
  217. };
  218. export type CovalentNFTData = {
  219. token_id: string;
  220. token_balance: string;
  221. external_data: CovalentNFTExternalData;
  222. token_url: string;
  223. };
  224. const getEthereumAccountsCovalent = async (
  225. walletAddress: string,
  226. nft?: boolean
  227. ): Promise<CovalentData[]> => {
  228. const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress, nft);
  229. try {
  230. const output = [] as CovalentData[];
  231. const response = await axios.get(url);
  232. const tokens = response.data.data.items;
  233. if (tokens instanceof Array && tokens.length) {
  234. for (const item of tokens) {
  235. // TODO: filter?
  236. if (
  237. item.contract_decimals !== undefined &&
  238. item.contract_ticker_symbol &&
  239. item.contract_address &&
  240. item.balance &&
  241. item.balance !== "0" &&
  242. (nft
  243. ? item.supports_erc?.includes("erc721")
  244. : item.supports_erc?.includes("erc20"))
  245. ) {
  246. output.push({ ...item } as CovalentData);
  247. }
  248. }
  249. }
  250. return output;
  251. } catch (error) {
  252. return Promise.reject("Unable to retrieve your Ethereum Tokens.");
  253. }
  254. };
  255. const getSolanaParsedTokenAccounts = (
  256. walletAddress: string,
  257. dispatch: Dispatch,
  258. nft: boolean
  259. ) => {
  260. const connection = new Connection(SOLANA_HOST, "finalized");
  261. dispatch(
  262. nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
  263. );
  264. return connection
  265. .getParsedTokenAccountsByOwner(new PublicKey(walletAddress), {
  266. programId: new PublicKey(TOKEN_PROGRAM_ID),
  267. })
  268. .then(
  269. (result) => {
  270. const mappedItems = result.value.map((item) =>
  271. createParsedTokenAccountFromInfo(item.pubkey, item.account)
  272. );
  273. dispatch(
  274. nft
  275. ? receiveSourceParsedTokenAccountsNFT(mappedItems)
  276. : receiveSourceParsedTokenAccounts(mappedItems)
  277. );
  278. },
  279. (error) => {
  280. dispatch(
  281. nft
  282. ? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata")
  283. : errorSourceParsedTokenAccounts("Failed to load token metadata.")
  284. );
  285. }
  286. );
  287. };
  288. /**
  289. * Fetches the balance of an asset for the connected wallet
  290. * This should handle every type of chain in the future, but only reads the Transfer state.
  291. */
  292. function useGetAvailableTokens(nft: boolean = false) {
  293. const dispatch = useDispatch();
  294. const tokenAccounts = useSelector(
  295. nft
  296. ? selectNFTSourceParsedTokenAccounts
  297. : selectTransferSourceParsedTokenAccounts
  298. );
  299. const lookupChain = useSelector(
  300. nft ? selectNFTSourceChain : selectTransferSourceChain
  301. );
  302. const solanaWallet = useSolanaWallet();
  303. const solPK = solanaWallet?.publicKey;
  304. const { provider, signer, signerAddress } = useEthereumProvider();
  305. const [covalent, setCovalent] = useState<any>(undefined);
  306. const [covalentLoading, setCovalentLoading] = useState(false);
  307. const [covalentError, setCovalentError] = useState<string | undefined>(
  308. undefined
  309. );
  310. const [ethNativeAccount, setEthNativeAccount] = useState<any>(undefined);
  311. const [ethNativeAccountLoading, setEthNativeAccountLoading] = useState(false);
  312. const [ethNativeAccountError, setEthNativeAccountError] = useState<
  313. string | undefined
  314. >(undefined);
  315. const [solanaMintAccounts, setSolanaMintAccounts] = useState<any>(undefined);
  316. const [solanaMintAccountsLoading, setSolanaMintAccountsLoading] =
  317. useState(false);
  318. const [solanaMintAccountsError, setSolanaMintAccountsError] = useState<
  319. string | undefined
  320. >(undefined);
  321. const selectedSourceWalletAddress = useSelector(
  322. nft ? selectNFTSourceWalletAddress : selectSourceWalletAddress
  323. );
  324. const currentSourceWalletAddress: string | undefined =
  325. lookupChain === CHAIN_ID_ETH
  326. ? signerAddress
  327. : lookupChain === CHAIN_ID_SOLANA
  328. ? solPK?.toString()
  329. : undefined;
  330. const resetSourceAccounts = useCallback(() => {
  331. dispatch(
  332. nft
  333. ? setSourceWalletAddressNFT(undefined)
  334. : setSourceWalletAddress(undefined)
  335. );
  336. dispatch(
  337. nft
  338. ? setSourceParsedTokenAccountNFT(undefined)
  339. : setSourceParsedTokenAccount(undefined)
  340. );
  341. dispatch(
  342. nft
  343. ? setSourceParsedTokenAccountsNFT(undefined)
  344. : setSourceParsedTokenAccounts(undefined)
  345. );
  346. !nft && dispatch(setAmount(""));
  347. setCovalent(undefined); //These need to be included in the reset because they have balances on them.
  348. setCovalentLoading(false);
  349. setCovalentError("");
  350. setEthNativeAccount(undefined);
  351. setEthNativeAccountLoading(false);
  352. setEthNativeAccountError("");
  353. }, [setCovalent, dispatch, nft]);
  354. //TODO this useEffect could be somewhere else in the codebase
  355. //It resets the SourceParsedTokens accounts when the wallet changes
  356. useEffect(() => {
  357. if (
  358. selectedSourceWalletAddress !== undefined &&
  359. currentSourceWalletAddress !== undefined &&
  360. currentSourceWalletAddress !== selectedSourceWalletAddress
  361. ) {
  362. resetSourceAccounts();
  363. return;
  364. } else {
  365. }
  366. }, [
  367. selectedSourceWalletAddress,
  368. currentSourceWalletAddress,
  369. dispatch,
  370. resetSourceAccounts,
  371. ]);
  372. //Solana accountinfos load
  373. useEffect(() => {
  374. if (lookupChain === CHAIN_ID_SOLANA && solPK) {
  375. if (
  376. !(tokenAccounts.data || tokenAccounts.isFetching || tokenAccounts.error)
  377. ) {
  378. getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
  379. }
  380. }
  381. return () => {};
  382. }, [dispatch, solanaWallet, lookupChain, solPK, tokenAccounts, nft]);
  383. //Solana Mint Accounts lookup
  384. useEffect(() => {
  385. if (lookupChain !== CHAIN_ID_SOLANA || !tokenAccounts.data?.length) {
  386. return () => {};
  387. }
  388. let cancelled = false;
  389. setSolanaMintAccountsLoading(true);
  390. setSolanaMintAccountsError(undefined);
  391. const mintAddresses = tokenAccounts.data.map((x) => x.mintKey);
  392. //This is a known wormhole v1 token on testnet
  393. // mintAddresses.push("4QixXecTZ4zdZGa39KH8gVND5NZ2xcaB12wiBhE4S7rn");
  394. //SOLT devnet token
  395. // mintAddresses.push("2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ");
  396. const connection = new Connection(SOLANA_HOST, "finalized");
  397. getMultipleAccountsRPC(
  398. connection,
  399. mintAddresses.map((x) => new PublicKey(x))
  400. ).then(
  401. (results) => {
  402. if (!cancelled) {
  403. const output = new Map<String, string | null>();
  404. results.forEach((result, index) =>
  405. output.set(
  406. mintAddresses[index],
  407. (result && extractMintAuthorityInfo(result)) || null
  408. )
  409. );
  410. setSolanaMintAccounts(output);
  411. setSolanaMintAccountsLoading(false);
  412. }
  413. },
  414. (error) => {
  415. if (!cancelled) {
  416. setSolanaMintAccounts(undefined);
  417. setSolanaMintAccountsLoading(false);
  418. setSolanaMintAccountsError(
  419. "Could not retrieve Solana mint accounts."
  420. );
  421. }
  422. }
  423. );
  424. return () => (cancelled = true);
  425. }, [tokenAccounts.data, lookupChain]);
  426. //Ethereum native asset load
  427. useEffect(() => {
  428. let cancelled = false;
  429. if (
  430. signerAddress &&
  431. lookupChain === CHAIN_ID_ETH &&
  432. !ethNativeAccount &&
  433. !nft
  434. ) {
  435. setEthNativeAccountLoading(true);
  436. createNativeEthParsedTokenAccount(provider, signerAddress).then(
  437. (result) => {
  438. console.log("create native account returned with value", result);
  439. if (!cancelled) {
  440. setEthNativeAccount(result);
  441. setEthNativeAccountLoading(false);
  442. setEthNativeAccountError("");
  443. }
  444. },
  445. (error) => {
  446. if (!cancelled) {
  447. setEthNativeAccount(undefined);
  448. setEthNativeAccountLoading(false);
  449. setEthNativeAccountError("Unable to retrieve your ETH balance.");
  450. }
  451. }
  452. );
  453. }
  454. return () => {
  455. cancelled = true;
  456. };
  457. }, [lookupChain, provider, signerAddress, nft, ethNativeAccount]);
  458. //Ethereum covalent accounts load
  459. useEffect(() => {
  460. //const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
  461. // const nftTestWallet1 = "0x3f304c6721f35ff9af00fd32650c8e0a982180ab";
  462. // const nftTestWallet2 = "0x98ed231428088eb440e8edb5cc8d66dcf913b86e";
  463. let cancelled = false;
  464. const walletAddress = signerAddress;
  465. if (walletAddress && lookupChain === CHAIN_ID_ETH && !covalent) {
  466. //TODO less cancel
  467. !cancelled && setCovalentLoading(true);
  468. !cancelled &&
  469. dispatch(
  470. nft
  471. ? fetchSourceParsedTokenAccountsNFT()
  472. : fetchSourceParsedTokenAccounts()
  473. );
  474. getEthereumAccountsCovalent(walletAddress, nft).then(
  475. (accounts) => {
  476. !cancelled && setCovalentLoading(false);
  477. !cancelled && setCovalentError(undefined);
  478. !cancelled && setCovalent(accounts);
  479. !cancelled &&
  480. dispatch(
  481. nft
  482. ? receiveSourceParsedTokenAccountsNFT(
  483. accounts.reduce((arr, current) => {
  484. if (current.nft_data) {
  485. current.nft_data.forEach((x) =>
  486. arr.push(
  487. createNFTParsedTokenAccountFromCovalent(
  488. walletAddress,
  489. current,
  490. x
  491. )
  492. )
  493. );
  494. }
  495. return arr;
  496. }, [] as NFTParsedTokenAccount[])
  497. )
  498. : receiveSourceParsedTokenAccounts(
  499. accounts.map((x) =>
  500. createParsedTokenAccountFromCovalent(walletAddress, x)
  501. )
  502. )
  503. );
  504. },
  505. () => {
  506. !cancelled &&
  507. dispatch(
  508. nft
  509. ? errorSourceParsedTokenAccountsNFT(
  510. "Cannot load your Ethereum NFTs at the moment."
  511. )
  512. : errorSourceParsedTokenAccounts(
  513. "Cannot load your Ethereum tokens at the moment."
  514. )
  515. );
  516. !cancelled &&
  517. setCovalentError("Cannot load your Ethereum tokens at the moment.");
  518. !cancelled && setCovalentLoading(false);
  519. }
  520. );
  521. return () => {
  522. cancelled = true;
  523. };
  524. }
  525. }, [lookupChain, provider, signerAddress, dispatch, nft, covalent]);
  526. //Terra accounts load
  527. //At present, we don't have any mechanism for doing this.
  528. useEffect(() => {}, []);
  529. const ethAccounts = useMemo(() => {
  530. const output = { ...tokenAccounts };
  531. output.data = output.data?.slice() || [];
  532. output.isFetching = output.isFetching || ethNativeAccountLoading;
  533. output.error = output.error || ethNativeAccountError;
  534. ethNativeAccount && output.data && output.data.unshift(ethNativeAccount);
  535. return output;
  536. }, [
  537. ethNativeAccount,
  538. ethNativeAccountLoading,
  539. ethNativeAccountError,
  540. tokenAccounts,
  541. ]);
  542. return lookupChain === CHAIN_ID_SOLANA
  543. ? {
  544. tokenAccounts: tokenAccounts,
  545. mintAccounts: {
  546. data: solanaMintAccounts,
  547. isFetching: solanaMintAccountsLoading,
  548. error: solanaMintAccountsError,
  549. receivedAt: null, //TODO
  550. },
  551. resetAccounts: resetSourceAccounts,
  552. }
  553. : lookupChain === CHAIN_ID_ETH
  554. ? {
  555. tokenAccounts: ethAccounts,
  556. covalent: {
  557. data: covalent,
  558. isFetching: covalentLoading,
  559. error: covalentError,
  560. receivedAt: null, //TODO
  561. },
  562. resetAccounts: resetSourceAccounts,
  563. }
  564. : lookupChain === CHAIN_ID_TERRA
  565. ? {
  566. resetAccounts: resetSourceAccounts,
  567. }
  568. : undefined;
  569. }
  570. export default useGetAvailableTokens;