diff --git a/src/components/TokenDialog.tsx b/src/components/TokenDialog.tsx
index d0f92172..e24251e3 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 8a6bd540..2283ca2b 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 d09bb617..0446aa9d 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);
+};