Add account selector

This commit is contained in:
Gary Wang 2020-07-31 21:00:40 -07:00
parent 6786852b0f
commit c54151662e
3 changed files with 152 additions and 14 deletions

View File

@ -8,6 +8,15 @@ 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 { clusterApiUrl } from '@solana/web3.js';
import { useWalletSelector } from '../utils/wallet';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import CheckIcon from '@material-ui/icons/Check';
import AddIcon from '@material-ui/icons/Add';
import AccountIcon from '@material-ui/icons/AccountCircle';
import Divider from '@material-ui/core/Divider';
import Hidden from '@material-ui/core/Hidden';
import IconButton from '@material-ui/core/IconButton';
import SolanaIcon from './SolanaIcon';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
content: { content: {
@ -17,8 +26,11 @@ const useStyles = makeStyles((theme) => ({
title: { title: {
flexGrow: 1, flexGrow: 1,
}, },
selectedNetwork: { button: {
fontWeight: 'bold', marginLeft: theme.spacing(1),
},
menuItemIcon: {
minWidth: 32,
}, },
})); }));
@ -31,6 +43,7 @@ export default function NavigationFrame({ children }) {
<Typography variant="h6" className={classes.title} component="h1"> <Typography variant="h6" className={classes.title} component="h1">
Solana SPL Token Wallet Solana SPL Token Wallet
</Typography> </Typography>
<WalletSelector />
<NetworkSelector /> <NetworkSelector />
</Toolbar> </Toolbar>
</AppBar> </AppBar>
@ -51,26 +64,50 @@ function NetworkSelector() {
'http://localhost:8899', 'http://localhost:8899',
]; ];
const networkLabels = {
[clusterApiUrl('mainnet-beta')]: 'Mainnet Beta',
[clusterApiUrl('devnet')]: 'Devnet',
[clusterApiUrl('testnet')]: 'Testnet',
};
return ( return (
<> <>
<Button color="inherit" onClick={(e) => setAnchorEl(e.target)}> <Hidden xsDown>
Network <Button
color="inherit"
onClick={(e) => setAnchorEl(e.target)}
className={classes.button}
>
{networkLabels[endpoint] ?? 'Network'}
</Button> </Button>
</Hidden>
<Hidden smUp>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.target)}>
<SolanaIcon />
</IconButton>
</Hidden>
<Menu <Menu
anchorEl={anchorEl} anchorEl={anchorEl}
open={!!anchorEl} open={!!anchorEl}
onClose={() => setAnchorEl(null)} onClose={() => setAnchorEl(null)}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
getContentAnchorEl={null}
> >
{networks.map((network) => ( {networks.map((network) => (
<MenuItem <MenuItem
key={network} key={network}
onClick={() => { onClick={() => {
setEndpoint(network);
setAnchorEl(null); setAnchorEl(null);
setEndpoint(network);
}} }}
selected={network === endpoint} selected={network === endpoint}
className={network === endpoint ? classes.selectedNetwork : null}
> >
<ListItemIcon className={classes.menuItemIcon}>
{network === endpoint ? <CheckIcon fontSize="small" /> : null}
</ListItemIcon>
{network} {network}
</MenuItem> </MenuItem>
))} ))}
@ -78,3 +115,66 @@ function NetworkSelector() {
</> </>
); );
} }
function WalletSelector() {
const { addresses, walletIndex, setWalletIndex } = useWalletSelector();
const [anchorEl, setAnchorEl] = useState(null);
const classes = useStyles();
return (
<>
<Hidden xsDown>
<Button
color="inherit"
onClick={(e) => setAnchorEl(e.target)}
className={classes.button}
>
Account
</Button>
</Hidden>
<Hidden smUp>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.target)}>
<AccountIcon />
</IconButton>
</Hidden>
<Menu
anchorEl={anchorEl}
open={!!anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
getContentAnchorEl={null}
>
{addresses.map((address, index) => (
<MenuItem
key={address.toBase58()}
onClick={() => {
setAnchorEl(null);
setWalletIndex(index);
}}
selected={index === walletIndex}
>
<ListItemIcon className={classes.menuItemIcon}>
{index === walletIndex ? <CheckIcon fontSize="small" /> : null}
</ListItemIcon>
{address.toBase58()}
</MenuItem>
))}
<Divider />
<MenuItem
onClick={() => {
setAnchorEl(null);
setWalletIndex(addresses.length);
}}
>
<ListItemIcon className={classes.menuItemIcon}>
<AddIcon fontSize="small" />
</ListItemIcon>
Create Account
</MenuItem>
</Menu>
</>
);
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default function SolanaIcon() {
return (
<SvgIcon viewBox="0 0 450 450">
<path d="m374.393945,136.203008c-2.518,2.519 -5.934,3.934 -9.496,3.934l-336.443,0c-11.891,0 -17.914,-14.315 -9.601,-22.817l55.186,-56.441c2.527,-2.584 5.988,-4.041 9.602,-4.041l337.697,0c11.964,0 17.955,14.465 9.496,22.925l-56.441,56.44z" />
<path d="m374.393945,392.628008c-2.518,2.518 -5.934,3.933 -9.496,3.933l-336.443,0c-11.891,0 -17.914,-14.315 -9.601,-22.817l55.186,-56.441c2.527,-2.584 5.988,-4.041 9.602,-4.041l337.697,0c11.964,0 17.955,14.465 9.496,22.925l-56.441,56.441z" />
<path d="m374.393945,188.933008c-2.518,-2.518 -5.934,-3.933 -9.496,-3.933l-336.443,0c-11.891,0 -17.914,14.315 -9.601,22.817l55.186,56.441c2.527,2.584 5.988,4.04 9.602,4.04l337.697,0c11.964,0 17.955,-14.464 9.496,-22.924l-56.441,-56.441z" />
</SvgIcon>
);
}

View File

@ -12,7 +12,7 @@ import {
parseTokenAccountData, parseTokenAccountData,
} from './token-state'; } from './token-state';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { useListener } from './utils'; import { useListener, useLocalStorageState } from './utils';
export class Wallet { export class Wallet {
constructor(connection, seed, walletIndex = 0) { constructor(connection, seed, walletIndex = 0) {
@ -25,11 +25,15 @@ export class Wallet {
this.emitter = new EventEmitter(); this.emitter = new EventEmitter();
} }
getAccount = (index) => { static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
const derivedSeed = bip32 const derivedSeed = bip32
.fromSeed(this.seed) .fromSeed(seed)
.derivePath(`m/501'/${this.walletIndex}'/0/${index}`).privateKey; .derivePath(`m/501'/${walletIndex}'/0/${accountIndex}`).privateKey;
return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey); return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey);
}
getAccount = (index) => {
return Wallet.getAccountFromSeed(this.seed, this.walletIndex, index);
}; };
getAccountBalance = async (index) => { getAccountBalance = async (index) => {
@ -116,12 +120,15 @@ export function WalletProvider({ children }) {
return localStorage.getItem('seed'); return localStorage.getItem('seed');
}, []); }, []);
const connection = useConnection(); const connection = useConnection();
const [walletIndex, setWalletIndex] = useLocalStorageState('walletIndex', 0);
const wallet = useMemo( const wallet = useMemo(
() => new Wallet(connection, Buffer.from(seed, 'hex')), () => new Wallet(connection, Buffer.from(seed, 'hex'), walletIndex),
[connection, seed], [connection, seed, walletIndex],
); );
return ( return (
<WalletContext.Provider value={{ wallet }}> <WalletContext.Provider
value={{ wallet, walletIndex, setWalletIndex, seed }}
>
{children} {children}
</WalletContext.Provider> </WalletContext.Provider>
); );
@ -137,6 +144,25 @@ export function useWalletAccountCount() {
return wallet.accountCount; return wallet.accountCount;
} }
export function useWalletSelector() {
const { walletIndex, setWalletIndex, seed } = useContext(WalletContext);
const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1);
function selectWallet(walletIndex) {
if (walletIndex >= walletCount) {
setWalletCount(walletIndex + 1);
}
setWalletIndex(walletIndex);
}
const addresses = useMemo(() => {
const seedBuffer = Buffer.from(seed, 'hex');
return [...Array(walletCount).keys()].map(
(walletIndex) =>
Wallet.getAccountFromSeed(seedBuffer, walletIndex).publicKey,
);
}, [seed, walletCount]);
return { addresses, walletIndex, setWalletIndex: selectWallet };
}
export async function mnemonicToSecretKey(mnemonic) { export async function mnemonicToSecretKey(mnemonic) {
const { mnemonicToSeed } = await import('bip39'); const { mnemonicToSeed } = await import('bip39');
const rootSeed = Buffer.from(await mnemonicToSeed(mnemonic), 'hex'); const rootSeed = Buffer.from(await mnemonicToSeed(mnemonic), 'hex');