Use @solana/spl-token-registry (#141)

This commit is contained in:
Pierre 2021-03-18 10:56:16 +11:00 committed by GitHub
parent 324b98d6ed
commit 4a601d8355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 188 additions and 164 deletions

View File

@ -7,6 +7,7 @@
"@material-ui/core": "^4.11.2", "@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@project-serum/serum": "^0.13.24", "@project-serum/serum": "^0.13.24",
"@solana/spl-token-registry": "^0.2.1",
"@solana/web3.js": "^0.87.2", "@solana/web3.js": "^0.87.2",
"@testing-library/jest-dom": "^5.11.6", "@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2", "@testing-library/react": "^11.2.2",

View File

@ -11,6 +11,7 @@ import { ConnectionProvider } from './utils/connection';
import WalletPage from './pages/WalletPage'; import WalletPage from './pages/WalletPage';
import { useWallet, WalletProvider } from './utils/wallet'; import { useWallet, WalletProvider } from './utils/wallet';
import { ConnectedWalletsProvider } from './utils/connected-wallets'; import { ConnectedWalletsProvider } from './utils/connected-wallets';
import { TokenRegistryProvider } from './utils/tokens/names';
import LoadingIndicator from './components/LoadingIndicator'; import LoadingIndicator from './components/LoadingIndicator';
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import PopupPage from './pages/PopupPage'; import PopupPage from './pages/PopupPage';
@ -62,9 +63,11 @@ export default function App() {
<CssBaseline /> <CssBaseline />
<ConnectionProvider> <ConnectionProvider>
<SnackbarProvider maxSnack={5} autoHideDuration={8000}> <TokenRegistryProvider>
<WalletProvider>{appElement}</WalletProvider> <SnackbarProvider maxSnack={5} autoHideDuration={8000}>
</SnackbarProvider> <WalletProvider>{appElement}</WalletProvider>
</SnackbarProvider>
</TokenRegistryProvider>
</ConnectionProvider> </ConnectionProvider>
</ThemeProvider> </ThemeProvider>
</Suspense> </Suspense>

View File

@ -11,7 +11,7 @@ import {
useWalletTokenAccounts, useWalletTokenAccounts,
} from '../utils/wallet'; } from '../utils/wallet';
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; 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 { useAsyncData } from '../utils/fetch-loop';
import LoadingIndicator from './LoadingIndicator'; import LoadingIndicator from './LoadingIndicator';
import { makeStyles, Tab, Tabs } from '@material-ui/core'; 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 ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore'; import ExpandMore from '@material-ui/icons/ExpandMore';
import Collapse from '@material-ui/core/Collapse'; import Collapse from '@material-ui/core/Collapse';
import { import { useSolanaExplorerUrlSuffix } from '../utils/connection';
useConnectionConfig,
useSolanaExplorerUrlSuffix,
} from '../utils/connection';
import Link from '@material-ui/core/Link'; import Link from '@material-ui/core/Link';
import CopyableDisplay from './CopyableDisplay'; import CopyableDisplay from './CopyableDisplay';
import DialogForm from './DialogForm'; import DialogForm from './DialogForm';
@ -56,10 +53,9 @@ export default function AddTokenDialog({ open, onClose }) {
let classes = useStyles(); let classes = useStyles();
let updateTokenName = useUpdateTokenName(); let updateTokenName = useUpdateTokenName();
const [sendTransaction, sending] = useSendTransaction(); 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 [tab, setTab] = useState(!!popularTokens ? 'popular' : 'manual');
const [mintAddress, setMintAddress] = useState(''); const [mintAddress, setMintAddress] = useState('');
const [tokenName, setTokenName] = useState(''); const [tokenName, setTokenName] = useState('');
@ -171,20 +167,18 @@ export default function AddTokenDialog({ open, onClose }) {
</React.Fragment> </React.Fragment>
) : tab === 'popular' ? ( ) : tab === 'popular' ? (
<List disablePadding> <List disablePadding>
{popularTokens {popularTokens.map((tokenInfo) => (
.filter((token) => !token.deprecated) <TokenListItem
.map((token) => ( key={tokenInfo.address}
<TokenListItem tokenInfo={tokenInfo}
key={token.mintAddress} existingAccount={(walletAccounts || []).find(
{...token} (account) =>
existingAccount={(walletAccounts || []).find( account.parsed.mint.toBase58() === tokenInfo.address,
(account) => )}
account.parsed.mint.toBase58() === token.mintAddress, onSubmit={onSubmit}
)} disabled={sending}
onSubmit={onSubmit} />
disalbed={sending} ))}
/>
))}
</List> </List>
) : tab === 'erc20' ? ( ) : tab === 'erc20' ? (
<> <>
@ -227,24 +221,21 @@ export default function AddTokenDialog({ open, onClose }) {
); );
} }
function TokenListItem({ function TokenListItem({ tokenInfo, onSubmit, disabled, existingAccount }) {
tokenName,
icon,
tokenSymbol,
mintAddress,
onSubmit,
disabled,
existingAccount,
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const urlSuffix = useSolanaExplorerUrlSuffix(); const urlSuffix = useSolanaExplorerUrlSuffix();
const alreadyExists = !!existingAccount; const alreadyExists = !!existingAccount;
return ( return (
<React.Fragment> <React.Fragment>
<div style={{ display: 'flex' }} key={tokenName}> <div style={{ display: 'flex' }} key={tokenInfo.name}>
<ListItem button onClick={() => setOpen((open) => !open)}> <ListItem button onClick={() => setOpen((open) => !open)}>
<ListItemIcon> <ListItemIcon>
<TokenIcon url={icon} tokenName={tokenName} size={20} /> <TokenIcon
url={tokenInfo.logoUri}
tokenName={tokenInfo.name}
size={20}
/>
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={ primary={
@ -252,12 +243,12 @@ function TokenListItem({
target="_blank" target="_blank"
rel="noopener" rel="noopener"
href={ href={
`https://explorer.solana.com/account/${mintAddress}` + `https://explorer.solana.com/account/${tokenInfo.address}` +
urlSuffix urlSuffix
} }
> >
{tokenName ?? abbreviateAddress(mintAddress)} {tokenInfo.name ?? abbreviateAddress(tokenInfo.address)}
{tokenSymbol ? ` (${tokenSymbol})` : null} {tokenInfo.symbol ? ` (${tokenInfo.symbol})` : null}
</Link> </Link>
} }
/> />
@ -267,15 +258,21 @@ function TokenListItem({
type="submit" type="submit"
color="primary" color="primary"
disabled={disabled || alreadyExists} disabled={disabled || alreadyExists}
onClick={() => onSubmit({ tokenName, tokenSymbol, mintAddress })} onClick={() =>
onSubmit({
tokenName: tokenInfo.name,
tokenSymbol: tokenInfo.symbol,
mintAddress: tokenInfo.address,
})
}
> >
{alreadyExists ? 'Added' : 'Add'} {alreadyExists ? 'Added' : 'Add'}
</Button> </Button>
</div> </div>
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<CopyableDisplay <CopyableDisplay
value={mintAddress} value={tokenInfo.address}
label={`${tokenSymbol} Mint Address`} label={`${tokenInfo.symbol} Mint Address`}
/> />
</Collapse> </Collapse>
</React.Fragment> </React.Fragment>

View File

@ -363,7 +363,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) {
return <LoadingIndicator delay={0} />; return <LoadingIndicator delay={0} />;
} }
let { amount, decimals, mint, tokenName, tokenSymbol } = balanceInfo; let { amount, decimals, mint, tokenName, tokenSymbol, tokenLogoUri } = balanceInfo;
tokenName = tokenName ?? abbreviateAddress(mint); tokenName = tokenName ?? abbreviateAddress(mint);
let displayName; let displayName;
if (isExtensionWidth) { if (isExtensionWidth) {
@ -450,7 +450,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) {
<> <>
<ListItem button onClick={() => expandable && setOpen((open) => !open)}> <ListItem button onClick={() => expandable && setOpen((open) => !open)}>
<ListItemIcon> <ListItemIcon>
<TokenIcon mint={mint} tokenName={tokenName} size={28} /> <TokenIcon mint={mint} tokenName={tokenName} url={tokenLogoUri} size={28} />
</ListItemIcon> </ListItemIcon>
<div style={{ display: 'flex', flex: 1 }}> <div style={{ display: 'flex', flex: 1 }}>
<ListItemText <ListItemText

View File

@ -24,7 +24,7 @@ import {
findAssociatedTokenAddress, findAssociatedTokenAddress,
} from '../utils/tokens'; } from '../utils/tokens';
import { sleep } from '../utils/utils'; import { sleep } from '../utils/utils';
import { getTokenName } from '../utils/tokens/names'; import { useTokenInfos, getTokenInfo } from '../utils/tokens/names';
export default function MergeAccountsDialog({ open, onClose }) { export default function MergeAccountsDialog({ open, onClose }) {
const [publicKeys] = useWalletPublicKeys(); const [publicKeys] = useWalletPublicKeys();
@ -33,6 +33,7 @@ export default function MergeAccountsDialog({ open, onClose }) {
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const [isMerging, setIsMerging] = useState(false); const [isMerging, setIsMerging] = useState(false);
const [mergeCheck, setMergeCheck] = useState(''); const [mergeCheck, setMergeCheck] = useState('');
const tokenInfos = useTokenInfos();
// Merging accounts is a destructive operation that, for each mint, // Merging accounts is a destructive operation that, for each mint,
// //
@ -98,8 +99,14 @@ export default function MergeAccountsDialog({ open, onClose }) {
assocTokAddr.equals(mintGroup[0].publicKey) assocTokAddr.equals(mintGroup[0].publicKey)
) )
) { ) {
const name = getTokenName(mint, connection._rpcEndpoint); const tokenInfo = getTokenInfo(
const symbol = name.symbol ? name.symbol : mint.toString(); mint,
connection._rpcEndpoint,
tokenInfos,
);
const symbol = tokenInfo.symbol
? tokenInfo.symbol
: mint.toString();
console.log(`Merging ${symbol}`); console.log(`Merging ${symbol}`);
enqueueSnackbar(`Merging ${symbol}`, { enqueueSnackbar(`Merging ${symbol}`, {
variant: 'info', variant: 'info',

View File

@ -1,13 +1,13 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { useConnectionConfig, MAINNET_URL } from '../utils/connection'; import { useConnectionConfig } from '../utils/connection';
import { CLUSTERS, clusterForEndpoint } from '../utils/clusters';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu'; import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import { clusterApiUrl } from '@solana/web3.js';
import { useWalletSelector } from '../utils/wallet'; import { useWalletSelector } from '../utils/wallet';
import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemIcon from '@material-ui/core/ListItemIcon';
import CheckIcon from '@material-ui/icons/Check'; import CheckIcon from '@material-ui/icons/Check';
@ -184,22 +184,10 @@ function ConnectionsButton() {
function NetworkSelector() { function NetworkSelector() {
const { endpoint, setEndpoint } = useConnectionConfig(); const { endpoint, setEndpoint } = useConnectionConfig();
const cluster = useMemo(() => clusterForEndpoint(endpoint), [endpoint])
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const classes = useStyles(); 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 ( return (
<> <>
<Hidden xsDown> <Hidden xsDown>
@ -208,7 +196,7 @@ function NetworkSelector() {
onClick={(e) => setAnchorEl(e.target)} onClick={(e) => setAnchorEl(e.target)}
className={classes.button} className={classes.button}
> >
{networkLabels[endpoint] ?? 'Network'} {cluster?.label ?? 'Network'}
</Button> </Button>
</Hidden> </Hidden>
<Hidden smUp> <Hidden smUp>
@ -228,19 +216,19 @@ function NetworkSelector() {
}} }}
getContentAnchorEl={null} getContentAnchorEl={null}
> >
{networks.map((network) => ( {CLUSTERS.map((cluster) => (
<MenuItem <MenuItem
key={network} key={cluster.apiUrl}
onClick={() => { onClick={() => {
setAnchorEl(null); setAnchorEl(null);
setEndpoint(network); setEndpoint(cluster.apiUrl);
}} }}
selected={network === endpoint} selected={cluster.apiUrl === endpoint}
> >
<ListItemIcon className={classes.menuItemIcon}> <ListItemIcon className={classes.menuItemIcon}>
{network === endpoint ? <CheckIcon fontSize="small" /> : null} {cluster.apiUrl === endpoint ? <CheckIcon fontSize="small" /> : null}
</ListItemIcon> </ListItemIcon>
{network} {cluster.apiUrl}
</MenuItem> </MenuItem>
))} ))}
</Menu> </Menu>

View File

@ -1,21 +1,10 @@
import { useConnectionConfig } from '../utils/connection';
import { TOKENS } from '../utils/tokens/names';
import React, { useState } from 'react'; import React, { useState } from 'react';
export default function TokenIcon({ mint, url, tokenName, size = 20 }) { export default function TokenIcon({ mint, url, tokenName, size = 20 }) {
const { endpoint } = useConnectionConfig();
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
if (!url) { if (!url && mint === null) {
if (mint === null) { url = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png';
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 (hasError || !url) { if (hasError || !url) {

29
src/utils/clusters.js Normal file
View File

@ -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);
}

View File

@ -1,9 +1,20 @@
import React, { useContext, useState, useEffect } from 'react';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { useConnectionConfig, MAINNET_URL } from '../connection'; import { useConnectionConfig, MAINNET_URL } from '../connection';
import { useListener } from '../utils'; import { useListener } from '../utils';
import { clusterForEndpoint } from '../clusters';
import { useCallback } from 'react'; 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]: [ [MAINNET_URL]: [
{ {
mintAddress: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', mintAddress: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt',
@ -214,62 +225,6 @@ export const TOKENS = {
icon: icon:
'https://raw.githubusercontent.com/raydium-io/media-assets/master/logo.svg', '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', tokenSymbol: 'OXY',
mintAddress: 'z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M', 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 (
<TokenListContext.Provider value={{ tokenInfos }}>
{props.children}
</TokenListContext.Provider>
);
}
const customTokenNamesByNetwork = JSON.parse( const customTokenNamesByNetwork = JSON.parse(
localStorage.getItem('tokenNames') ?? '{}', localStorage.getItem('tokenNames') ?? '{}',
); );
@ -287,25 +275,32 @@ const customTokenNamesByNetwork = JSON.parse(
const nameUpdated = new EventEmitter(); const nameUpdated = new EventEmitter();
nameUpdated.setMaxListeners(100); nameUpdated.setMaxListeners(100);
export function useTokenName(mint) { export function useTokenInfo(mint) {
const { endpoint } = useConnectionConfig(); const { endpoint } = useConnectionConfig();
useListener(nameUpdated, 'update'); 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) { if (!mint) {
return { name: null, symbol: null }; return { name: null, symbol: null };
} }
let info = customTokenNamesByNetwork?.[endpoint]?.[mint.toBase58()]; let info = customTokenNamesByNetwork?.[endpoint]?.[mint.toBase58()];
let match = TOKENS?.[endpoint]?.find( let match = tokenInfos?.find(
(token) => token.mintAddress === mint.toBase58(), (tokenInfo) => tokenInfo.address === mint.toBase58(),
); );
if (match && (!info || match.deprecated)) { if (match) {
info = { name: match.tokenName, symbol: match.tokenSymbol }; 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() { export function useUpdateTokenName() {
@ -334,3 +329,14 @@ export function useUpdateTokenName() {
[endpoint], [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),
);
}

View File

@ -16,14 +16,14 @@ import {
transferTokens, transferTokens,
transferAndClose, transferAndClose,
} from './tokens'; } from './tokens';
import { TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from './tokens/instructions'; import { TOKEN_PROGRAM_ID } from './tokens/instructions';
import { import {
ACCOUNT_LAYOUT, ACCOUNT_LAYOUT,
parseMintData, parseMintData,
parseTokenAccountData, parseTokenAccountData,
} from './tokens/data'; } from './tokens/data';
import { useListener, useLocalStorageState, useRefEqual } from './utils'; import { useListener, useLocalStorageState, useRefEqual } from './utils';
import { useTokenName } from './tokens/names'; import { useTokenInfo } from './tokens/names';
import { refreshCache, useAsyncData } from './fetch-loop'; import { refreshCache, useAsyncData } from './fetch-loop';
import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed'; import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed';
import { WalletProviderFactory } from './walletProvider/factory'; import { WalletProviderFactory } from './walletProvider/factory';
@ -419,24 +419,12 @@ export function useBalanceInfo(publicKey) {
? parseTokenAccountData(accountInfo.data) ? parseTokenAccountData(accountInfo.data)
: {}; : {};
let [mintInfo, mintInfoLoaded] = useAccountInfo(mint); let [mintInfo, mintInfoLoaded] = useAccountInfo(mint);
let { name, symbol } = useTokenName(mint); let { name, symbol, logoUri } = useTokenInfo(mint);
if (!accountInfoLoaded) { if (!accountInfoLoaded) {
return null; 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) { if (mint && mintInfoLoaded) {
try { try {
let { decimals } = parseMintData(mintInfo.data); let { decimals } = parseMintData(mintInfo.data);
@ -447,6 +435,7 @@ export function useBalanceInfo(publicKey) {
owner, owner,
tokenName: name, tokenName: name,
tokenSymbol: symbol, tokenSymbol: symbol,
tokenLogoUri: logoUri,
valid: true, valid: true,
}; };
} catch (e) { } catch (e) {
@ -457,6 +446,7 @@ export function useBalanceInfo(publicKey) {
owner, owner,
tokenName: 'Invalid', tokenName: 'Invalid',
tokenSymbol: 'INVALID', tokenSymbol: 'INVALID',
tokenLogoUri: null,
valid: false, valid: false,
}; };
} }

View File

@ -1918,6 +1918,13 @@
dependencies: dependencies:
"@sinonjs/commons" "^1.7.0" "@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": "@solana/web3.js@^0.87.2":
version "0.87.2" version "0.87.2"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.87.2.tgz#92c8d344695c6113d4e0eb3339117fbc6b22d0d2" 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" safe-buffer "^5.0.1"
sha.js "^2.4.8" 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: cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 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" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
node-fetch@^2.2.0: node-fetch@2.6.1, node-fetch@^2.2.0:
version "2.6.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==