Skip to content

Commit

Permalink
feat: show owned-tokens
Browse files Browse the repository at this point in the history
feat: show owned-tokens
  • Loading branch information
smrnjeet222 committed Sep 29, 2021
1 parent 3f213aa commit 1d0f407
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 21 deletions.
40 changes: 24 additions & 16 deletions src/components/TokenDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down Expand Up @@ -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 (
<ListItem
button
onClick={() => onClick(mint)}
style={{ padding: "10px 20px" }}
>
<TokenIcon mint={mint} style={{ width: "30px", borderRadius: "15px" }} />
<TokenName tokenInfo={tokenInfo} />
<ListItemAvatar>
<TokenIcon
mint={mint}
style={{ width: "30px", borderRadius: "15px" }}
/>
</ListItemAvatar>
<ListItemText primary={tokenInfo?.symbol} secondary={tokenInfo?.name} />
{+details?.balance > 0 && (
<Box mr={1} textAlign="end">
<ListItemText
primary={details?.balance}
secondary={`$${details?.usd}`}
/>
</Box>
)}
</ListItem>
);
}

function TokenName({ tokenInfo }: { tokenInfo: TokenInfo }) {
return (
<div style={{ marginLeft: "16px" }}>
<Typography style={{ fontWeight: "bold" }}>
{tokenInfo?.symbol}
</Typography>
<Typography color="textSecondary" style={{ fontSize: "14px" }}>
{tokenInfo?.name}
</Typography>
</div>
);
}
66 changes: 62 additions & 4 deletions src/context/TokenList.tsx
Original file line number Diff line number Diff line change
@@ -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<string, TokenInfo>;
Expand All @@ -9,6 +15,7 @@ type TokenListContext = {
swappableTokens: TokenInfo[];
swappableTokensSollet: TokenInfo[];
swappableTokensWormhole: TokenInfo[];
ownedTokensDetailed: OwnedTokenDetailed[];
};
const _TokenListContext = React.createContext<null | TokenListContext>(null);

Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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(() => {
Expand Down Expand Up @@ -106,6 +163,7 @@ export function TokenListContextProvider(props: any) {
swappableTokens,
swappableTokensWormhole,
swappableTokensSollet,
ownedTokensDetailed,
}}
>
{props.children}
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default function Swap(props: SwapProps): ReactElement {
);
return (
<ThemeProvider theme={theme}>
<TokenListContextProvider tokenList={tokenList}>
<TokenListContextProvider tokenList={tokenList} provider={provider}>
<TokenContextProvider provider={provider}>
<DexContextProvider swapClient={swapClient}>
<SwapContextProvider
Expand Down
49 changes: 49 additions & 0 deletions src/utils/userTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type OwnedTokenDetailed = {
address: string;
balance: string;
usd: number;
};

export const fetchSolPrice = async (): Promise<number> => {
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<OwnedTokenDetailed[]> => {
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);
};

0 comments on commit 1d0f407

Please sign in to comment.