Skip to content

Commit

Permalink
BIP 44 derivation path (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
armaniferrante authored Feb 12, 2021
1 parent f0386ce commit 3fc7be0
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 79 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"bn.js": "^5.1.2",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"ed25519-hd-key": "^1.2.0",
"immutable-tuple": "^0.4.10",
"mdi-material-ui": "^6.21.0",
"notistack": "^1.0.2",
Expand Down
7 changes: 4 additions & 3 deletions src/components/BalancesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ const useStyles = makeStyles((theme) => ({
},
}));

function BalanceListItem({ publicKey }) {
export function BalanceListItem({ publicKey, expandable }) {
const balanceInfo = useBalanceInfo(publicKey);
const classes = useStyles();
const [open, setOpen] = useState(false);
expandable = expandable === undefined ? true : expandable;

if (!balanceInfo) {
return <LoadingIndicator delay={0} />;
Expand All @@ -151,7 +152,7 @@ function BalanceListItem({ publicKey }) {

return (
<>
<ListItem button onClick={() => setOpen((open) => !open)}>
<ListItem button onClick={() => expandable && setOpen((open) => !open)}>
<ListItemIcon>
<TokenIcon mint={mint} tokenName={tokenName} size={28} />
</ListItemIcon>
Expand All @@ -166,7 +167,7 @@ function BalanceListItem({ publicKey }) {
secondary={publicKey.toBase58()}
secondaryTypographyProps={{ className: classes.address }}
/>
{open ? <ExpandLess /> : <ExpandMore />}
{expandable ? open ? <ExpandLess /> : <ExpandMore /> : <></>}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<BalanceListItemDetails
Expand Down
244 changes: 193 additions & 51 deletions src/pages/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,28 @@ import {
mnemonicToSeed,
storeMnemonicAndSeed,
} from '../utils/wallet-seed';
import {
getAccountFromSeed,
DERIVATION_PATH,
} from '../utils/walletProvider/localStorage.js';
import { useSolanaExplorerUrlSuffix } from '../utils/connection';
import Container from '@material-ui/core/Container';
import LoadingIndicator from '../components/LoadingIndicator';
import { BalanceListItem } from '../components/BalancesList.js';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { Typography } from '@material-ui/core';
import TextField from '@material-ui/core/TextField';
import Checkbox from '@material-ui/core/Checkbox';
import FormControl from '@material-ui/core/FormControl';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import CardActions from '@material-ui/core/CardActions';
import Button from '@material-ui/core/Button';
import Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
import { useCallAsync } from '../utils/notifications';
import Link from '@material-ui/core/Link';
import { validateMnemonic } from 'bip39';

export default function LoginPage() {
const [restore, setRestore] = useState(false);
Expand Down Expand Up @@ -48,10 +58,18 @@ function CreateWalletForm() {

function submit(password) {
const { mnemonic, seed } = mnemonicAndSeed;
callAsync(storeMnemonicAndSeed(mnemonic, seed, password), {
progressMessage: 'Creating wallet...',
successMessage: 'Wallet created',
});
callAsync(
storeMnemonicAndSeed(
mnemonic,
seed,
password,
DERIVATION_PATH.bip44Change,
),
{
progressMessage: 'Creating wallet...',
successMessage: 'Wallet created',
},
);
}

if (!savedWords) {
Expand Down Expand Up @@ -85,8 +103,8 @@ function SeedWordsForm({ mnemonicAndSeed, goForward }) {
Create a new wallet to hold Solana and SPL tokens.
</Typography>
<Typography>
Please write down the following twelve words and keep them in a safe
place:
Please write down the following twenty four words and keep them in a
safe place:
</Typography>
{mnemonicAndSeed ? (
<TextField
Expand All @@ -106,6 +124,11 @@ function SeedWordsForm({ mnemonicAndSeed, goForward }) {
You will need these words to restore your wallet if your browser's
storage is cleared or your device is damaged or lost.
</Typography>
<Typography paragraph>
By default, sollet will use <code>m/44'/501'/0'/0'</code> as the
derivation path for the main wallet. To use an alternative path, try
restoring an existing wallet.
</Typography>
<FormControlLabel
control={
<Checkbox
Expand Down Expand Up @@ -227,69 +250,188 @@ function LoginForm() {

function RestoreWalletForm({ goBack }) {
const [mnemonic, setMnemonic] = useState('');
const [seed, setSeed] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [next, setNext] = useState(false);
const isNextBtnEnabled =
password === passwordConfirm && validateMnemonic(mnemonic);

return (
<>
{next ? (
<DerivedAccounts
goBack={() => setNext(false)}
mnemonic={mnemonic}
password={password}
seed={seed}
/>
) : (
<Card>
<CardContent>
<Typography variant="h5" gutterBottom>
Restore Existing Wallet
</Typography>
<Typography>
Restore your wallet using your twelve or twenty-four seed words. Note that this
will delete any existing wallet on this device.
</Typography>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
margin="normal"
label="Seed Words"
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
/>
<TextField
variant="outlined"
fullWidth
margin="normal"
label="New Password (Optional)"
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<TextField
variant="outlined"
fullWidth
margin="normal"
label="Confirm Password"
type="password"
autoComplete="new-password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
/>
</CardContent>
<CardActions style={{ justifyContent: 'space-between' }}>
<Button onClick={goBack}>Cancel</Button>
<Button
color="primary"
disabled={!isNextBtnEnabled}
onClick={() => {
mnemonicToSeed(mnemonic).then((seed) => {
setSeed(seed);
setNext(true);
});
}}
>
Next
</Button>
</CardActions>
</Card>
)}
</>
);
}

function DerivedAccounts({ goBack, mnemonic, seed, password }) {
const callAsync = useCallAsync();
const urlSuffix = useSolanaExplorerUrlSuffix();
const [dPathMenuItem, setDPathMenuItem] = useState(
DerivationPathMenuItem.Bip44Change,
);

const accounts = [...Array(10)].map((_, idx) => {
return getAccountFromSeed(
Buffer.from(seed, 'hex'),
idx,
toDerivationPath(dPathMenuItem),
);
});

function submit() {
callAsync(
mnemonicToSeed(mnemonic).then((seed) =>
storeMnemonicAndSeed(mnemonic, seed, password),
storeMnemonicAndSeed(
mnemonic,
seed,
password,
toDerivationPath(dPathMenuItem),
),
);
}

return (
<Card>
<CardContent>
<Typography variant="h5" gutterBottom>
Restore Existing Wallet
</Typography>
<Typography>
Restore your wallet using your twelve seed words. Note that this will
delete any existing wallet on this device.
</Typography>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
margin="normal"
label="Seed Words"
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
/>
<TextField
variant="outlined"
fullWidth
margin="normal"
label="New Password (Optional)"
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<TextField
variant="outlined"
fullWidth
margin="normal"
label="Confirm Password"
type="password"
autoComplete="new-password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
/>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<Typography variant="h5" gutterBottom>
Derivable Accounts
</Typography>
<FormControl variant="outlined">
<Select
value={dPathMenuItem}
onChange={(e) => setDPathMenuItem(e.target.value)}
>
<MenuItem value={DerivationPathMenuItem.Bip44Change}>
{`m/44'/501'/0'/0'`}
</MenuItem>
<MenuItem value={DerivationPathMenuItem.Bip44}>
{`m/44'/501'/0'`}
</MenuItem>
<MenuItem value={DerivationPathMenuItem.Deprecated}>
{`m/501'/0'/0/0 (deprecated)`}
</MenuItem>
</Select>
</FormControl>
</div>
{accounts.map((acc) => {
return (
<Link
href={
`https://explorer.solana.com/account/${acc.publicKey.toBase58()}` +
urlSuffix
}
target="_blank"
rel="noopener"
>
<BalanceListItem
publicKey={acc.publicKey}
walletAccount={acc}
expandable={false}
/>
</Link>
);
})}
</CardContent>
<CardActions style={{ justifyContent: 'space-between' }}>
<Button onClick={goBack}>Cancel</Button>
<Button
color="primary"
disabled={password !== passwordConfirm}
onClick={submit}
>
<Button onClick={goBack}>Back</Button>
<Button color="primary" onClick={submit}>
Restore
</Button>
</CardActions>
</Card>
);
}

// Material UI's Select doesn't render properly when using an `undefined` value,
// so we define this type and the subsequent `toDerivationPath` translator as a
// workaround.
//
// DERIVATION_PATH.deprecated is always undefined.
const DerivationPathMenuItem = {
Deprecated: 0,
Bip44: 1,
Bip44Change: 2,
};

function toDerivationPath(dPathMenuItem) {
switch (dPathMenuItem) {
case DerivationPathMenuItem.Deprecated:
return DERIVATION_PATH.deprecated;
case DerivationPathMenuItem.Bip44:
return DERIVATION_PATH.bip44;
case DerivationPathMenuItem.Bip44Change:
return DERIVATION_PATH.bip44Change;
default:
throw new Error(`invalid derivation path: ${dPathMenuItem}`);
}
}
Loading

0 comments on commit 3fc7be0

Please sign in to comment.