diff --git a/example/src/App.tsx b/example/src/App.tsx index af8df775..2fd38965 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -46,6 +46,13 @@ function AppInner() { const [isConnected, setIsConnected] = useState(false); const [tokenList, setTokenList] = useState(null); + let commonBases: PublicKey[] = [ + new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), + new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), + new PublicKey("So11111111111111111111111111111111111111112"), + new PublicKey("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt"), + ]; + const [provider, wallet] = useMemo(() => { const opts: ConfirmOptions = { preflightCommitment: "recent", @@ -103,7 +110,7 @@ function AppInner() { return ( @@ -114,7 +121,13 @@ function AppInner() { > {!isConnected ? "Connect" : "Disconnect"} - {tokenList && } + {tokenList && ( + + )} ); } diff --git a/src/components/Swap.tsx b/src/components/Swap.tsx index 2cba7b6a..5a6aea71 100644 --- a/src/components/Swap.tsx +++ b/src/components/Swap.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import { PublicKey, Keypair, @@ -222,6 +222,16 @@ export function SwapTokenForm({ }) : amount; + const tokenDialog = useMemo(() => { + return ( + setShowTokenDialog(false)} + /> + ); + }, [showTokenDialog]); + return (
@@ -252,11 +262,7 @@ export function SwapTokenForm({ }, }} /> - setShowTokenDialog(false)} - /> + {tokenDialog}
); } diff --git a/src/components/TokenDialog.tsx b/src/components/TokenDialog.tsx index d0f92172..6f406b35 100644 --- a/src/components/TokenDialog.tsx +++ b/src/components/TokenDialog.tsx @@ -10,12 +10,16 @@ import { TextField, List, ListItem, + ListSubheader, Typography, + Chip, + Avatar, Tabs, Tab, } from "@material-ui/core"; +import { StarOutline, Star } from "@material-ui/icons"; import { TokenIcon } from "./Swap"; -import { useSwappableTokens } from "../context/TokenList"; +import { useSwappableTokens, useTokenBase } from "../context/TokenList"; import { useMediaQuery } from "@material-ui/core"; const useStyles = makeStyles((theme) => ({ @@ -54,6 +58,7 @@ export default function TokenDialog({ const styles = useStyles(); const { swappableTokens, swappableTokensSollet, swappableTokensWormhole } = useSwappableTokens(); + const { tokenBase, addNewBase, tokenBaseMap, removeBase } = useTokenBase(); const displayTabs = !useMediaQuery("(max-width:450px)"); const selectedTokens = tabSelection === 0 @@ -97,6 +102,17 @@ export default function TokenDialog({ + {tokenBase?.length != 0 && ( + + { + setMint(mint); + onClose(); + }} + /> + + )} {tokens.map((tokenInfo: TokenInfo) => ( { + addNewBase(token); + }} + isCommonBase={ + tokenBaseMap.get(tokenInfo.address.toString()) ? true : false + } + removeBase={(token) => { + removeBase(token); + }} /> ))} @@ -146,19 +171,41 @@ export default function TokenDialog({ function TokenListItem({ tokenInfo, onClick, + addNewBase, + removeBase, + isCommonBase, }: { tokenInfo: TokenInfo; onClick: (mint: PublicKey) => void; + addNewBase: (token: TokenInfo) => void; + removeBase: (token: TokenInfo) => void; + isCommonBase: Boolean; }) { const mint = new PublicKey(tokenInfo.address); return ( - onClick(mint)} - style={{ padding: "10px 20px" }} - > - - + +
onClick(mint)} + style={{ + padding: "10px 20px", + display: "flex", + cursor: "pointer", + width: "100%", + }} + > + + +
+ : } + onClick={() => + isCommonBase ? removeBase(tokenInfo) : addNewBase(tokenInfo) + } + />
); } @@ -175,3 +222,30 @@ function TokenName({ tokenInfo }: { tokenInfo: TokenInfo }) {
); } + +function CommonBases({ + commonTokenBases, + onClick, +}: { + commonTokenBases: TokenInfo[] | undefined; + onClick: (mint: PublicKey) => void; +}) { + return ( +
+

Common bases

+ {commonTokenBases?.map((tokenInfo: TokenInfo) => { + const mint = new PublicKey(tokenInfo.address); + return ( + } + variant="outlined" + label={tokenInfo?.symbol} + onClick={() => onClick(mint)} + style={{ margin: "0 1px" }} + /> + ); + })} +
+ ); +} diff --git a/src/context/TokenList.tsx b/src/context/TokenList.tsx index 8a6bd540..d7ef47f9 100644 --- a/src/context/TokenList.tsx +++ b/src/context/TokenList.tsx @@ -1,6 +1,12 @@ -import React, { useContext, useMemo } from "react"; +import React, { useContext, useMemo, useState, useEffect } from "react"; import { TokenInfo } from "@solana/spl-token-registry"; import { SOL_MINT } from "../utils/pubkeys"; +import { PublicKey } from "@solana/web3.js"; +import { LocalStorage } from "../utils/localStorage"; + +interface TokenCommonBaseInfo extends TokenInfo { + isCommonBase: boolean; +} type TokenListContext = { tokenMap: Map; @@ -9,6 +15,10 @@ type TokenListContext = { swappableTokens: TokenInfo[]; swappableTokensSollet: TokenInfo[]; swappableTokensWormhole: TokenInfo[]; + tokenBase: TokenInfo[] | undefined; + addNewBase: (token: TokenInfo) => void; + removeBase: (token: TokenInfo) => void; + tokenBaseMap: Map; }; const _TokenListContext = React.createContext(null); @@ -97,6 +107,65 @@ export function TokenListContextProvider(props: any) { ]; }, [tokenList]); + let [tokenBase, setTokenBase] = useState( + undefined + ); + let [addrList, setValues, removeValue] = LocalStorage("swapui-common-bases"); + + // Common token bases + useEffect(() => { + if (addrList == null) { + addrList = props.commonBases ?? []; + } + if (props.commonBases) { + props.commonBases.forEach((add: any) => setValues(add.toString())); + addrList.concat(props.commonBases); + } + addrList = addrList.map((e: string) => new PublicKey(e.toString())); + const cb = addrList?.map((add: PublicKey) => { + const token = tokenMap.get(add.toString()); + token.isCommonBase = true; + setValues(token.address); + return token; + }); + setTokenBase(cb); + return cb; + }, [props.commonBases]); + + const addNewBase = (token: TokenInfo) => { + // Check if token already a common base + if ( + tokenBase?.some((t) => token.address.toString() === t.address.toString()) + ) { + return; + } + const c: TokenCommonBaseInfo = { ...token, isCommonBase: true }; + setValues(token.address); + setTokenBase((prevState) => [...(prevState as TokenCommonBaseInfo[]), c]); + }; + + const removeBase = (token: TokenInfo) => { + const index = + tokenBase?.findIndex( + (t) => token.address.toString() === t.address.toString() + ) ?? -1; + // return if not found + if (index == -1) return; + const tempTokenBase = tokenBase?.slice(); + tempTokenBase?.splice(index, 1); + setTokenBase(tempTokenBase); + removeValue(index); + }; + + // Token map for quick lookup. + const tokenBaseMap = useMemo(() => { + const tokenBaseMap = new Map(); + tokenBase?.forEach((t: TokenCommonBaseInfo) => { + tokenBaseMap.set(t.address, t); + }); + return tokenBaseMap; + }, [tokenBase]); + return ( <_TokenListContext.Provider value={{ @@ -106,6 +175,10 @@ export function TokenListContextProvider(props: any) { swappableTokens, swappableTokensWormhole, swappableTokensSollet, + tokenBase, + addNewBase, + removeBase, + tokenBaseMap, }} > {props.children} @@ -129,5 +202,20 @@ export function useTokenMap(): Map { export function useSwappableTokens() { const { swappableTokens, swappableTokensWormhole, swappableTokensSollet } = useTokenListContext(); - return { swappableTokens, swappableTokensWormhole, swappableTokensSollet }; + return { + swappableTokens, + swappableTokensWormhole, + swappableTokensSollet, + }; +} + +export function useTokenBase() { + const { tokenBase, addNewBase, tokenBaseMap, removeBase } = + useTokenListContext(); + return { + tokenBase, + addNewBase, + tokenBaseMap, + removeBase, + }; } diff --git a/src/index.tsx b/src/index.tsx index d09bb617..f5c1388d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import { TokenListContainer } from "@solana/spl-token-registry"; import { Provider } from "@project-serum/anchor"; import { Swap as SwapClient } from "@project-serum/swap"; import { - createMuiTheme, + createTheme, ThemeOptions, ThemeProvider, } from "@material-ui/core/styles"; @@ -52,6 +52,7 @@ export default function Swap(props: SwapProps): ReactElement { materialTheme, provider, tokenList, + commonBases, fromMint, toMint, fromAmount, @@ -61,7 +62,7 @@ export default function Swap(props: SwapProps): ReactElement { // @ts-ignore const swapClient = new SwapClient(provider, tokenList); - const theme = createMuiTheme( + const theme = createTheme( materialTheme || { palette: { primary: { @@ -80,7 +81,7 @@ export default function Swap(props: SwapProps): ReactElement { ); return ( - + void, (val: number) => void] { + const currentValue = localStorage.getItem(name); + let addrList = JSON.parse(currentValue as string); + + function setValues(val: any): void { + addrList = addrList ? addrList : []; + if (addrList.indexOf(val) == -1) { + addrList.push(val); + localStorage.setItem(name, JSON.stringify(addrList)); + } + } + + function removeValue(index: number): void { + addrList.splice(index, 1); + localStorage.setItem(name, JSON.stringify(addrList)); + } + + return [addrList, setValues, removeValue]; +}