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 MenuItem from '@material-ui/core/MenuItem';
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) => ({
content: {
@ -17,8 +26,11 @@ const useStyles = makeStyles((theme) => ({
title: {
flexGrow: 1,
},
selectedNetwork: {
fontWeight: 'bold',
button: {
marginLeft: theme.spacing(1),
},
menuItemIcon: {
minWidth: 32,
},
}));
@ -31,6 +43,7 @@ export default function NavigationFrame({ children }) {
<Typography variant="h6" className={classes.title} component="h1">
Solana SPL Token Wallet
</Typography>
<WalletSelector />
<NetworkSelector />
</Toolbar>
</AppBar>
@ -51,26 +64,50 @@ function NetworkSelector() {
'http://localhost:8899',
];
const networkLabels = {
[clusterApiUrl('mainnet-beta')]: 'Mainnet Beta',
[clusterApiUrl('devnet')]: 'Devnet',
[clusterApiUrl('testnet')]: 'Testnet',
};
return (
<>
<Button color="inherit" onClick={(e) => setAnchorEl(e.target)}>
Network
</Button>
<Hidden xsDown>
<Button
color="inherit"
onClick={(e) => setAnchorEl(e.target)}
className={classes.button}
>
{networkLabels[endpoint] ?? 'Network'}
</Button>
</Hidden>
<Hidden smUp>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.target)}>
<SolanaIcon />
</IconButton>
</Hidden>
<Menu
anchorEl={anchorEl}
open={!!anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
getContentAnchorEl={null}
>
{networks.map((network) => (
<MenuItem
key={network}
onClick={() => {
setEndpoint(network);
setAnchorEl(null);
setEndpoint(network);
}}
selected={network === endpoint}
className={network === endpoint ? classes.selectedNetwork : null}
>
<ListItemIcon className={classes.menuItemIcon}>
{network === endpoint ? <CheckIcon fontSize="small" /> : null}
</ListItemIcon>
{network}
</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,
} from './token-state';
import EventEmitter from 'events';
import { useListener } from './utils';
import { useListener, useLocalStorageState } from './utils';
export class Wallet {
constructor(connection, seed, walletIndex = 0) {
@ -25,11 +25,15 @@ export class Wallet {
this.emitter = new EventEmitter();
}
getAccount = (index) => {
static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
const derivedSeed = bip32
.fromSeed(this.seed)
.derivePath(`m/501'/${this.walletIndex}'/0/${index}`).privateKey;
.fromSeed(seed)
.derivePath(`m/501'/${walletIndex}'/0/${accountIndex}`).privateKey;
return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey);
}
getAccount = (index) => {
return Wallet.getAccountFromSeed(this.seed, this.walletIndex, index);
};
getAccountBalance = async (index) => {
@ -116,12 +120,15 @@ export function WalletProvider({ children }) {
return localStorage.getItem('seed');
}, []);
const connection = useConnection();
const [walletIndex, setWalletIndex] = useLocalStorageState('walletIndex', 0);
const wallet = useMemo(
() => new Wallet(connection, Buffer.from(seed, 'hex')),
[connection, seed],
() => new Wallet(connection, Buffer.from(seed, 'hex'), walletIndex),
[connection, seed, walletIndex],
);
return (
<WalletContext.Provider value={{ wallet }}>
<WalletContext.Provider
value={{ wallet, walletIndex, setWalletIndex, seed }}
>
{children}
</WalletContext.Provider>
);
@ -137,6 +144,25 @@ export function useWalletAccountCount() {
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) {
const { mnemonicToSeed } = await import('bip39');
const rootSeed = Buffer.from(await mnemonicToSeed(mnemonic), 'hex');