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 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
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');
|
||||||
|
|
Loading…
Reference in New Issue