From 4a601d835542999b724a3c285b25570269de0ae7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 18 Mar 2021 10:56:16 +1100 Subject: [PATCH] Use @solana/spl-token-registry (#141) --- package.json | 1 + src/App.js | 9 +- src/components/AddTokenDialog.js | 75 +++++++------- src/components/BalancesList.js | 4 +- src/components/MergeAccountsDialog.js | 13 ++- src/components/NavigationFrame.js | 34 +++---- src/components/TokenIcon.js | 15 +-- src/utils/clusters.js | 29 ++++++ src/utils/tokens/names.js | 136 ++++++++++++++------------ src/utils/wallet.js | 20 +--- yarn.lock | 16 ++- 11 files changed, 188 insertions(+), 164 deletions(-) create mode 100644 src/utils/clusters.js diff --git a/package.json b/package.json index 6cbf519..db79420 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@material-ui/core": "^4.11.2", "@material-ui/icons": "^4.11.2", "@project-serum/serum": "^0.13.24", + "@solana/spl-token-registry": "^0.2.1", "@solana/web3.js": "^0.87.2", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", diff --git a/src/App.js b/src/App.js index 7526e64..091c47e 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import { ConnectionProvider } from './utils/connection'; import WalletPage from './pages/WalletPage'; import { useWallet, WalletProvider } from './utils/wallet'; import { ConnectedWalletsProvider } from './utils/connected-wallets'; +import { TokenRegistryProvider } from './utils/tokens/names'; import LoadingIndicator from './components/LoadingIndicator'; import { SnackbarProvider } from 'notistack'; import PopupPage from './pages/PopupPage'; @@ -62,9 +63,11 @@ export default function App() { - - {appElement} - + + + {appElement} + + diff --git a/src/components/AddTokenDialog.js b/src/components/AddTokenDialog.js index 3cfea4f..f976eee 100644 --- a/src/components/AddTokenDialog.js +++ b/src/components/AddTokenDialog.js @@ -11,7 +11,7 @@ import { useWalletTokenAccounts, } from '../utils/wallet'; import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; -import { TOKENS, useUpdateTokenName } from '../utils/tokens/names'; +import { useUpdateTokenName, usePopularTokens } from '../utils/tokens/names'; import { useAsyncData } from '../utils/fetch-loop'; import LoadingIndicator from './LoadingIndicator'; import { makeStyles, Tab, Tabs } from '@material-ui/core'; @@ -24,10 +24,7 @@ import { abbreviateAddress } from '../utils/utils'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; import Collapse from '@material-ui/core/Collapse'; -import { - useConnectionConfig, - useSolanaExplorerUrlSuffix, -} from '../utils/connection'; +import { useSolanaExplorerUrlSuffix } from '../utils/connection'; import Link from '@material-ui/core/Link'; import CopyableDisplay from './CopyableDisplay'; import DialogForm from './DialogForm'; @@ -56,10 +53,9 @@ export default function AddTokenDialog({ open, onClose }) { let classes = useStyles(); let updateTokenName = useUpdateTokenName(); const [sendTransaction, sending] = useSendTransaction(); - const { endpoint } = useConnectionConfig(); - const popularTokens = TOKENS[endpoint]; - const [walletAccounts] = useWalletTokenAccounts(); + const [walletAccounts] = useWalletTokenAccounts(); + const popularTokens = usePopularTokens(); const [tab, setTab] = useState(!!popularTokens ? 'popular' : 'manual'); const [mintAddress, setMintAddress] = useState(''); const [tokenName, setTokenName] = useState(''); @@ -171,20 +167,18 @@ export default function AddTokenDialog({ open, onClose }) { ) : tab === 'popular' ? ( - {popularTokens - .filter((token) => !token.deprecated) - .map((token) => ( - - account.parsed.mint.toBase58() === token.mintAddress, - )} - onSubmit={onSubmit} - disalbed={sending} - /> - ))} + {popularTokens.map((tokenInfo) => ( + + account.parsed.mint.toBase58() === tokenInfo.address, + )} + onSubmit={onSubmit} + disabled={sending} + /> + ))} ) : tab === 'erc20' ? ( <> @@ -227,24 +221,21 @@ export default function AddTokenDialog({ open, onClose }) { ); } -function TokenListItem({ - tokenName, - icon, - tokenSymbol, - mintAddress, - onSubmit, - disabled, - existingAccount, -}) { +function TokenListItem({ tokenInfo, onSubmit, disabled, existingAccount }) { const [open, setOpen] = useState(false); const urlSuffix = useSolanaExplorerUrlSuffix(); const alreadyExists = !!existingAccount; + return ( -
+
setOpen((open) => !open)}> - + - {tokenName ?? abbreviateAddress(mintAddress)} - {tokenSymbol ? ` (${tokenSymbol})` : null} + {tokenInfo.name ?? abbreviateAddress(tokenInfo.address)} + {tokenInfo.symbol ? ` (${tokenInfo.symbol})` : null} } /> @@ -267,15 +258,21 @@ function TokenListItem({ type="submit" color="primary" disabled={disabled || alreadyExists} - onClick={() => onSubmit({ tokenName, tokenSymbol, mintAddress })} + onClick={() => + onSubmit({ + tokenName: tokenInfo.name, + tokenSymbol: tokenInfo.symbol, + mintAddress: tokenInfo.address, + }) + } > {alreadyExists ? 'Added' : 'Add'}
diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index 564c1d1..834e09c 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -363,7 +363,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { return ; } - let { amount, decimals, mint, tokenName, tokenSymbol } = balanceInfo; + let { amount, decimals, mint, tokenName, tokenSymbol, tokenLogoUri } = balanceInfo; tokenName = tokenName ?? abbreviateAddress(mint); let displayName; if (isExtensionWidth) { @@ -450,7 +450,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { <> expandable && setOpen((open) => !open)}> - +
clusterForEndpoint(endpoint), [endpoint]) const [anchorEl, setAnchorEl] = useState(null); const classes = useStyles(); - const networks = [ - MAINNET_URL, - clusterApiUrl('devnet'), - clusterApiUrl('testnet'), - 'http://localhost:8899', - ]; - - const networkLabels = { - [MAINNET_URL]: 'Mainnet Beta', - [clusterApiUrl('devnet')]: 'Devnet', - [clusterApiUrl('testnet')]: 'Testnet', - }; - return ( <> @@ -208,7 +196,7 @@ function NetworkSelector() { onClick={(e) => setAnchorEl(e.target)} className={classes.button} > - {networkLabels[endpoint] ?? 'Network'} + {cluster?.label ?? 'Network'} @@ -228,19 +216,19 @@ function NetworkSelector() { }} getContentAnchorEl={null} > - {networks.map((network) => ( + {CLUSTERS.map((cluster) => ( { setAnchorEl(null); - setEndpoint(network); + setEndpoint(cluster.apiUrl); }} - selected={network === endpoint} + selected={cluster.apiUrl === endpoint} > - {network === endpoint ? : null} + {cluster.apiUrl === endpoint ? : null} - {network} + {cluster.apiUrl} ))} diff --git a/src/components/TokenIcon.js b/src/components/TokenIcon.js index 7af4b30..0dce703 100644 --- a/src/components/TokenIcon.js +++ b/src/components/TokenIcon.js @@ -1,21 +1,10 @@ -import { useConnectionConfig } from '../utils/connection'; -import { TOKENS } from '../utils/tokens/names'; import React, { useState } from 'react'; export default function TokenIcon({ mint, url, tokenName, size = 20 }) { - const { endpoint } = useConnectionConfig(); - const [hasError, setHasError] = useState(false); - if (!url) { - if (mint === null) { - url = - 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png'; - } else { - url = TOKENS?.[endpoint]?.find( - (token) => token.mintAddress === mint?.toBase58(), - )?.icon; - } + if (!url && mint === null) { + url = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png'; } if (hasError || !url) { diff --git a/src/utils/clusters.js b/src/utils/clusters.js new file mode 100644 index 0000000..78c74c8 --- /dev/null +++ b/src/utils/clusters.js @@ -0,0 +1,29 @@ +import { clusterApiUrl } from '@solana/web3.js'; +import { MAINNET_URL } from '../utils/connection'; + +export const CLUSTERS = [ + { + name: 'mainnet-beta', + apiUrl: MAINNET_URL, + label: 'Mainnet Beta' + }, + { + name: 'devnet', + apiUrl: clusterApiUrl('devnet'), + label: 'Devnet' + }, + { + name: 'testnet', + apiUrl: clusterApiUrl('testnet'), + label: 'Testnet' + }, + { + name: 'localnet', + apiUrl: 'http://localhost:8899', + label: null + } +]; + +export function clusterForEndpoint(endpoint) { + return CLUSTERS.find(({ apiUrl }) => apiUrl === endpoint); +} \ No newline at end of file diff --git a/src/utils/tokens/names.js b/src/utils/tokens/names.js index 6702cad..f6a422c 100644 --- a/src/utils/tokens/names.js +++ b/src/utils/tokens/names.js @@ -1,9 +1,20 @@ +import React, { useContext, useState, useEffect } from 'react'; import EventEmitter from 'events'; import { useConnectionConfig, MAINNET_URL } from '../connection'; import { useListener } from '../utils'; +import { clusterForEndpoint } from '../clusters'; import { useCallback } from 'react'; +import { PublicKey } from '@solana/web3.js'; +import { TokenListProvider } from '@solana/spl-token-registry'; -export const TOKENS = { +// This list is used for deciding what to display in the popular tokens list +// in the `AddTokenDialog`. +// +// Icons, names, and symbols are fetched not from here, but from the +// @solana/spl-token-registry. To add an icon or token name to the wallet, +// add the mints to that package. To add a token to the `AddTokenDialog`, +// add the `mintAddress` here. The rest of the fields are not used. +const POPULAR_TOKENS = { [MAINNET_URL]: [ { mintAddress: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', @@ -214,62 +225,6 @@ export const TOKENS = { icon: 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', }, - { - tokenSymbol: 'RAY-LEGACY-USDT', - mintAddress: 'CzPDyvotTcxNqtPne32yUiEVQ6jk42HZi1Y3hUu7qf7f', - tokenName: 'Raydium Legacy USDT Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-LEGACY-USDC', - mintAddress: 'FgmBnsF5Qrnv8X9bomQfEtQTQjNNiBCWRKGpzPnE5BDg', - tokenName: 'Raydium Legacy USDC Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-LEGACY-SRM', - mintAddress: '5QXBMXuCL7zfAk39jEVVEvcrz1AvBGgT9wAhLLHLyyUJ', - tokenName: 'Raydium Legacy Serum Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-ETH', - mintAddress: 'Q6MKy5Yxb9vG1mWzppMtMb2nrhNuCRNUkJTeiE3fuwD', - tokenName: 'Raydium ETH Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-SOL', - mintAddress: 'F5PPQHGcznZ2FxD9JaxJMXaf7XkaFFJ6zzTBcW8osQjw', - tokenName: 'Raydium SOL Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-SRM', - mintAddress: 'DSX5E21RE9FB9hM8Nh8xcXQfPK6SzRaJiywemHBSsfup', - tokenName: 'Raydium SRM Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-USDT', - mintAddress: 'FdhKXYjCou2jQfgKWcNY7jb8F2DPLU1teTTTRfLBD2v1', - tokenName: 'Raydium USDT Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, - { - tokenSymbol: 'RAY-USDC', - mintAddress: 'BZFGfXMrjG2sS7QT2eiCDEevPFnkYYF7kzJpWfYxPbcx', - tokenName: 'Raydium USDC Liquidity Pool', - icon: - 'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', - }, { tokenSymbol: 'OXY', mintAddress: 'z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M', @@ -280,6 +235,39 @@ export const TOKENS = { ], }; +const TokenListContext = React.createContext({}); + +export function useTokenInfos() { + const { tokenInfos } = useContext(TokenListContext); + return tokenInfos; +} + +export function TokenRegistryProvider(props) { + const { endpoint } = useConnectionConfig(); + const [tokenInfos, setTokenInfos] = useState(null); + useEffect(() => { + const tokenListProvider = new TokenListProvider(); + tokenListProvider.resolve().then((tokenListContainer) => { + const cluster = clusterForEndpoint(endpoint); + + const filteredTokenListContainer = tokenListContainer?.filterByClusterSlug( + cluster?.name, + ); + const tokenInfos = + tokenListContainer !== filteredTokenListContainer + ? filteredTokenListContainer?.getList() + : null; // Workaround for filter return all on unknown slug + setTokenInfos(tokenInfos); + }); + }, [endpoint]); + + return ( + + {props.children} + + ); +} + const customTokenNamesByNetwork = JSON.parse( localStorage.getItem('tokenNames') ?? '{}', ); @@ -287,25 +275,32 @@ const customTokenNamesByNetwork = JSON.parse( const nameUpdated = new EventEmitter(); nameUpdated.setMaxListeners(100); -export function useTokenName(mint) { +export function useTokenInfo(mint) { const { endpoint } = useConnectionConfig(); useListener(nameUpdated, 'update'); - return getTokenName(mint, endpoint); + const tokenInfos = useTokenInfos(); + return getTokenInfo(mint, endpoint, tokenInfos); } -export function getTokenName(mint, endpoint) { +export function getTokenInfo(mint, endpoint, tokenInfos) { if (!mint) { return { name: null, symbol: null }; } let info = customTokenNamesByNetwork?.[endpoint]?.[mint.toBase58()]; - let match = TOKENS?.[endpoint]?.find( - (token) => token.mintAddress === mint.toBase58(), + let match = tokenInfos?.find( + (tokenInfo) => tokenInfo.address === mint.toBase58(), ); - if (match && (!info || match.deprecated)) { - info = { name: match.tokenName, symbol: match.tokenSymbol }; + if (match) { + if (!info) { + info = { ...match, logoUri: match.logoURI }; + } + // The user has overridden a name locally. + else { + info = { ...info, logoUri: match.logoURI }; + } } - return { name: info?.name, symbol: info?.symbol }; + return { ...info }; } export function useUpdateTokenName() { @@ -334,3 +329,14 @@ export function useUpdateTokenName() { [endpoint], ); } +// Returns tokenInfos for the popular tokens list. +export function usePopularTokens() { + const tokenInfos = useTokenInfos(); + const { endpoint } = useConnectionConfig(); + return (!POPULAR_TOKENS[endpoint] + ? [] + : POPULAR_TOKENS[endpoint] + ).map((tok) => + getTokenInfo(new PublicKey(tok.mintAddress), endpoint, tokenInfos), + ); +} diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 72decee..bddbded 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -16,14 +16,14 @@ import { transferTokens, transferAndClose, } from './tokens'; -import { TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from './tokens/instructions'; +import { TOKEN_PROGRAM_ID } from './tokens/instructions'; import { ACCOUNT_LAYOUT, parseMintData, parseTokenAccountData, } from './tokens/data'; import { useListener, useLocalStorageState, useRefEqual } from './utils'; -import { useTokenName } from './tokens/names'; +import { useTokenInfo } from './tokens/names'; import { refreshCache, useAsyncData } from './fetch-loop'; import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed'; import { WalletProviderFactory } from './walletProvider/factory'; @@ -419,24 +419,12 @@ export function useBalanceInfo(publicKey) { ? parseTokenAccountData(accountInfo.data) : {}; let [mintInfo, mintInfoLoaded] = useAccountInfo(mint); - let { name, symbol } = useTokenName(mint); + let { name, symbol, logoUri } = useTokenInfo(mint); if (!accountInfoLoaded) { return null; } - if (mint && mint.equals(WRAPPED_SOL_MINT)) { - return { - amount, - decimals: 9, - mint, - owner, - tokenName: 'Wrapped SOL', - tokenSymbol: 'SOL', - valid: true, - }; - } - if (mint && mintInfoLoaded) { try { let { decimals } = parseMintData(mintInfo.data); @@ -447,6 +435,7 @@ export function useBalanceInfo(publicKey) { owner, tokenName: name, tokenSymbol: symbol, + tokenLogoUri: logoUri, valid: true, }; } catch (e) { @@ -457,6 +446,7 @@ export function useBalanceInfo(publicKey) { owner, tokenName: 'Invalid', tokenSymbol: 'INVALID', + tokenLogoUri: null, valid: false, }; } diff --git a/yarn.lock b/yarn.lock index 763388a..45ca7de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,6 +1918,13 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@solana/spl-token-registry@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.1.tgz#79b3a8c3f3a12b7956b6ecc08052438972da5ce3" + integrity sha512-JzvezgnPftowZXwAzUR7ywm16G6eWb9E7h3iUfeMmzZStBdqB/16TE8+x09yJoYtW5B6bTa80tELB7EfBzZ15w== + dependencies: + cross-fetch "^3.0.6" + "@solana/web3.js@^0.87.2": version "0.87.2" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.87.2.tgz#92c8d344695c6113d4e0eb3339117fbc6b22d0d2" @@ -4456,6 +4463,13 @@ create-hmac@1.1.7, create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" + integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -9085,7 +9099,7 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-fetch@^2.2.0: +node-fetch@2.6.1, node-fetch@^2.2.0: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==