Add account selector
This commit is contained in:
parent
6786852b0f
commit
c54151662e
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue