diff --git a/src/components/TokenDialog.tsx b/src/components/TokenDialog.tsx index d0f9217..e24251e 100644 --- a/src/components/TokenDialog.tsx +++ b/src/components/TokenDialog.tsx @@ -13,9 +13,12 @@ import { Typography, Tabs, Tab, + ListItemText, + ListItemAvatar, + Box, } from "@material-ui/core"; import { TokenIcon } from "./Swap"; -import { useSwappableTokens } from "../context/TokenList"; +import { useSwappableTokens, useTokenListContext } from "../context/TokenList"; import { useMediaQuery } from "@material-ui/core"; const useStyles = makeStyles((theme) => ({ @@ -151,27 +154,32 @@ function TokenListItem({ onClick: (mint: PublicKey) => void; }) { const mint = new PublicKey(tokenInfo.address); + const { ownedTokensDetailed } = useTokenListContext(); + const details = ownedTokensDetailed.filter( + (t) => t.address === tokenInfo.address + )?.[0]; + return ( onClick(mint)} style={{ padding: "10px 20px" }} > - - + + + + + {+details?.balance > 0 && ( + + + + )} ); } - -function TokenName({ tokenInfo }: { tokenInfo: TokenInfo }) { - return ( -
- - {tokenInfo?.symbol} - - - {tokenInfo?.name} - -
- ); -} diff --git a/src/context/TokenList.tsx b/src/context/TokenList.tsx index 8a6bd54..2283ca2 100644 --- a/src/context/TokenList.tsx +++ b/src/context/TokenList.tsx @@ -1,6 +1,12 @@ -import React, { useContext, useMemo } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import { TokenInfo } from "@solana/spl-token-registry"; import { SOL_MINT } from "../utils/pubkeys"; +import { PublicKey } from "@solana/web3.js"; +import { + fetchSolPrice, + getUserTokens, + OwnedTokenDetailed, +} from "../utils/userTokens"; type TokenListContext = { tokenMap: Map; @@ -9,6 +15,7 @@ type TokenListContext = { swappableTokens: TokenInfo[]; swappableTokensSollet: TokenInfo[]; swappableTokensWormhole: TokenInfo[]; + ownedTokensDetailed: OwnedTokenDetailed[]; }; const _TokenListContext = React.createContext(null); @@ -37,6 +44,10 @@ const SOL_TOKEN_INFO = { }; export function TokenListContextProvider(props: any) { + const [ownedTokensDetailed, setOwnedTokensDetailed] = useState< + OwnedTokenDetailed[] + >([]); + const tokenList = useMemo(() => { const list = props.tokenList.filterByClusterSlug("mainnet-beta").getList(); // Manually add a fake SOL mint for the native token. The component is @@ -45,6 +56,8 @@ export function TokenListContextProvider(props: any) { return list; }, [props.tokenList]); + const pk: PublicKey | undefined = props?.provider?.wallet?.publicKey; + // Token map for quick lookup. const tokenMap = useMemo(() => { const tokenMap = new Map(); @@ -54,18 +67,62 @@ export function TokenListContextProvider(props: any) { return tokenMap; }, [tokenList]); + useEffect(() => { + (async () => { + let solBalance: number = 0; + if (pk) solBalance = await props.provider.connection.getBalance(pk); + const tokens = await getUserTokens(pk?.toString()); + const solPrice = await fetchSolPrice(); + + solBalance = solBalance / 10 ** +SOL_TOKEN_INFO.decimals; + + const SolDetails = { + address: SOL_TOKEN_INFO.address, + balance: solBalance.toFixed(6), + usd: +(solBalance * solPrice).toFixed(4), + }; + // only show the sol token if wallet is connected + if (pk) { + setOwnedTokensDetailed([SolDetails, ...tokens]); + } else { + // on disconnect, tokens = [] + setOwnedTokensDetailed(tokens); + } + })(); + }, [pk]); + // Tokens with USD(x) quoted markets. const swappableTokens = useMemo(() => { - const tokens = tokenList.filter((t: TokenInfo) => { + const allTokens = tokenList.filter((t: TokenInfo) => { const isUsdxQuoted = t.extensions?.serumV3Usdt || t.extensions?.serumV3Usdc; return isUsdxQuoted; }); - tokens.sort((a: TokenInfo, b: TokenInfo) => + + const ownedTokensList = ownedTokensDetailed.map((t) => t.address); + + // Partition allTokens (pass & fail reduce) + const [ownedTokens, notOwnedtokens] = allTokens.reduce( + ([p, f]: [TokenInfo[], TokenInfo[]], t: TokenInfo) => + // pass & fail condition + ownedTokensList.includes(t.address) ? [[...p, t], f] : [p, [...f, t]], + [[], []] + ); + notOwnedtokens.sort((a: TokenInfo, b: TokenInfo) => a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0 ); + // sort by price in USD + ownedTokens.sort( + (a: TokenInfo, b: TokenInfo) => + +ownedTokensDetailed.filter((t: any) => t.address === b.address)?.[0] + .usd - + +ownedTokensDetailed.filter((t: any) => t.address === a.address)?.[0] + .usd + ); + const tokens = ownedTokens.concat(notOwnedtokens); + return tokens; - }, [tokenList, tokenMap]); + }, [tokenList, tokenMap, ownedTokensDetailed]); // Sollet wrapped tokens. const [swappableTokensSollet, solletMap] = useMemo(() => { @@ -106,6 +163,7 @@ export function TokenListContextProvider(props: any) { swappableTokens, swappableTokensWormhole, swappableTokensSollet, + ownedTokensDetailed, }} > {props.children} diff --git a/src/index.tsx b/src/index.tsx index d09bb61..0446aa9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -80,7 +80,7 @@ export default function Swap(props: SwapProps): ReactElement { ); return ( - + => { + try { + const response = await fetch("https://api.solscan.io/market?symbol=SOL"); + const json = await response.json(); + return json.data.priceUsdt; + } catch (error) { + console.error(error); + return 0; + } +}; + +// TODO: use web3 library +export const getUserTokens = async ( + pk?: string +): Promise => { + let data: OwnedTokenDetailed[] = []; + + // for testing + // pk = "CuieVDEDtLo7FypA9SbLM9saXFdb1dsshEkyErMqkRQq" + + try { + if (pk) { + let tokens = await ( + await fetch( + `https://api.solscan.io/account/tokens?address=${pk}&price=1` + ) + ).json(); + data = tokens.data.map((token: any) => { + return { + address: token.tokenAddress, + balance: token.tokenAmount.uiAmountString, + usd: +(token.tokenAmount.uiAmount * (token.priceUsdt ?? 0)).toFixed( + 4 + ), + }; + }); + } + } catch (error) { + console.error(error); + } + + return data.filter((t: OwnedTokenDetailed) => +t.balance > 0); +};