parent
3f213aa5ae
commit
1d0f407680
|
@ -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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
@ -9,6 +15,7 @@ type TokenListContext = {
|
|||
swappableTokens: TokenInfo[];
|
||||
swappableTokensSollet: TokenInfo[];
|
||||
swappableTokensWormhole: TokenInfo[];
|
||||
ownedTokensDetailed: OwnedTokenDetailed[];
|
||||
};
|
||||
const _TokenListContext = React.createContext<null | TokenListContext>(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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
Loading…
Reference in New Issue