Revert "Import accounts with private key (#29)"

This reverts commit b362d16862.
This commit is contained in:
Nathaniel Parke 2020-11-10 09:21:29 +08:00
parent 7509130cc5
commit 3c3ba629ec
5 changed files with 70 additions and 230 deletions

View File

@ -4,83 +4,35 @@ import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import Switch from '@material-ui/core/Switch';
import { Account } from '@solana/web3.js';
import * as bs58 from 'bs58';
import DialogForm from './DialogForm'; import DialogForm from './DialogForm';
export default function AddAccountDialog({ open, onAdd, onClose }) { export default function AddAccountDialog({ open, onAdd, onClose }) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [isImport, setIsImport] = useState(false);
const [importedPrivateKey, setPrivateKey] = useState('');
const importedAccount = isImport ? decodeAccount(importedPrivateKey) : undefined;
const isAddEnabled = isImport ? name && importedAccount !== undefined : name;
return ( return (
<DialogForm <DialogForm
open={open} open={open}
onClose={onClose} onClose={onClose}
onSubmit={() => onAdd({ name, importedAccount })} onSubmit={() => onAdd(name)}
fullWidth fullWidth
> >
<DialogTitle>Add account</DialogTitle> <DialogTitle>Add account</DialogTitle>
<DialogContent style={{ paddingTop: 16 }}> <DialogContent style={{ paddingTop: 16 }}>
<div <TextField
style={{ label="Name"
display: 'flex', fullWidth
flexDirection: 'column', variant="outlined"
}} margin="normal"
> value={name}
<TextField onChange={(e) => setName(e.target.value.trim())}
label="Name" />
fullWidth
variant="outlined"
margin="normal"
value={name}
onChange={(e) => setName(e.target.value.trim())}
/>
<FormGroup>
<FormControlLabel
control={
<Switch
checked={isImport}
onChange={() => setIsImport(!isImport)}
/>
}
label="Import private key"
/>
</FormGroup>
{isImport && (
<TextField
label="Paste your private key here"
fullWidth
type="password"
value={importedPrivateKey}
variant="outlined"
margin="normal"
onChange={(e) => setPrivateKey(e.target.value.trim())}
/>
)}
</div>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}>Close</Button> <Button onClick={onClose}>Close</Button>
<Button type="submit" color="primary" disabled={!isAddEnabled}> <Button type="submit" color="primary" disabled={!name}>
Add Add
</Button> </Button>
</DialogActions> </DialogActions>
</DialogForm> </DialogForm>
); );
} }
function decodeAccount(privateKey) {
try {
const a = new Account(bs58.decode(privateKey));
return a;
} catch (_) {
return undefined;
}
}

View File

@ -126,7 +126,7 @@ function NetworkSelector() {
} }
function WalletSelector() { function WalletSelector() {
const { accounts, setWalletSelector, addAccount } = useWalletSelector(); const { accounts, walletIndex, setWalletIndex } = useWalletSelector();
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [addAccountOpen, setAddAccountOpen] = useState(false); const [addAccountOpen, setAddAccountOpen] = useState(false);
const classes = useStyles(); const classes = useStyles();
@ -140,14 +140,8 @@ function WalletSelector() {
<AddAccountDialog <AddAccountDialog
open={addAccountOpen} open={addAccountOpen}
onClose={() => setAddAccountOpen(false)} onClose={() => setAddAccountOpen(false)}
onAdd={({ name, importedAccount }) => { onAdd={(name) => {
addAccount({ name, importedAccount }); setWalletIndex(accounts.length, name);
setWalletSelector({
walletIndex: importedAccount ? undefined : accounts.length,
importedPubkey: importedAccount
? importedAccount.publicKey.toString()
: undefined,
});
setAddAccountOpen(false); setAddAccountOpen(false);
}} }}
/> />
@ -177,21 +171,23 @@ function WalletSelector() {
}} }}
getContentAnchorEl={null} getContentAnchorEl={null}
> >
{accounts.map(({ isSelected, selector, address, name, label }) => ( {accounts.map(({ index, address, name }) => (
<MenuItem <MenuItem
key={address.toBase58()} key={address.toBase58()}
onClick={() => { onClick={() => {
setAnchorEl(null); setAnchorEl(null);
setWalletSelector(selector); setWalletIndex(index);
}} }}
selected={isSelected} selected={index === walletIndex}
component="div" component="div"
> >
<ListItemIcon className={classes.menuItemIcon}> <ListItemIcon className={classes.menuItemIcon}>
{isSelected ? <CheckIcon fontSize="small" /> : null} {index === walletIndex ? <CheckIcon fontSize="small" /> : null}
</ListItemIcon> </ListItemIcon>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography>{name}</Typography> <Typography>
{index === 0 ? 'Main account' : name || `Account ${index}`}
</Typography>
<Typography color="textSecondary"> <Typography color="textSecondary">
{address.toBase58()} {address.toBase58()}
</Typography> </Typography>
@ -208,7 +204,7 @@ function WalletSelector() {
<ListItemIcon className={classes.menuItemIcon}> <ListItemIcon className={classes.menuItemIcon}>
<AddIcon fontSize="small" /> <AddIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
Add Account Create Account
</MenuItem> </MenuItem>
</Menu> </Menu>
</> </>

View File

@ -28,10 +28,7 @@ import Link from '@material-ui/core/Link';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useAsyncData } from '../utils/fetch-loop'; import { useAsyncData } from '../utils/fetch-loop';
import CircularProgress from '@material-ui/core/CircularProgress'; import CircularProgress from '@material-ui/core/CircularProgress';
import { import {TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT} from '../utils/tokens/instructions';
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
} from '../utils/tokens/instructions';
import { parseTokenAccountData } from '../utils/tokens/data'; import { parseTokenAccountData } from '../utils/tokens/data';
const WUSDC_MINT = new PublicKey( const WUSDC_MINT = new PublicKey(
@ -137,10 +134,9 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
} }
function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) { function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
const defaultAddressHelperText = const defaultAddressHelperText = !balanceInfo.mint || balanceInfo.mint.equals(WRAPPED_SOL_MINT) ?
!balanceInfo.mint || balanceInfo.mint.equals(WRAPPED_SOL_MINT) 'Enter Solana Address' :
? 'Enter Solana Address' 'Enter SPL token or Solana address';
: 'Enter SPL token or Solana address';
const wallet = useWallet(); const wallet = useWallet();
const [sendTransaction, sending] = useSendTransaction(); const [sendTransaction, sending] = useSendTransaction();
const [addressHelperText, setAddressHelperText] = useState( const [addressHelperText, setAddressHelperText] = useState(

View File

@ -1,6 +1,5 @@
import { pbkdf2 } from 'crypto'; import { pbkdf2 } from 'crypto';
import { randomBytes, secretbox } from 'tweetnacl'; import { randomBytes, secretbox } from 'tweetnacl';
import * as bip32 from 'bip32';
import bs58 from 'bs58'; import bs58 from 'bs58';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
@ -20,20 +19,11 @@ export async function mnemonicToSeed(mnemonic) {
return Buffer.from(seed).toString('hex'); return Buffer.from(seed).toString('hex');
} }
let unlockedMnemonicAndSeed = (() => { let unlockedMnemonicAndSeed = JSON.parse(
const stored = JSON.parse( sessionStorage.getItem('unlocked') ||
sessionStorage.getItem('unlocked') || localStorage.getItem('unlocked') ||
localStorage.getItem('unlocked') || 'null',
'null', ) || { mnemonic: null, seed: null };
);
if (stored === null) {
return { mnemonic: null, seed: null, importsEncryptionKey: null };
}
return {
importsEncryptionKey: deriveImportsEncryptionKey(stored.seed),
...stored,
};
})();
export const walletSeedChanged = new EventEmitter(); export const walletSeedChanged = new EventEmitter();
export function getUnlockedMnemonicAndSeed() { export function getUnlockedMnemonicAndSeed() {
@ -44,8 +34,8 @@ export function hasLockedMnemonicAndSeed() {
return !!localStorage.getItem('locked'); return !!localStorage.getItem('locked');
} }
function setUnlockedMnemonicAndSeed(mnemonic, seed, importsEncryptionKey) { function setUnlockedMnemonicAndSeed(mnemonic, seed) {
unlockedMnemonicAndSeed = { mnemonic, seed, importsEncryptionKey }; unlockedMnemonicAndSeed = { mnemonic, seed };
walletSeedChanged.emit('change', unlockedMnemonicAndSeed); walletSeedChanged.emit('change', unlockedMnemonicAndSeed);
} }
@ -77,8 +67,7 @@ export async function storeMnemonicAndSeed(mnemonic, seed, password) {
localStorage.removeItem('locked'); localStorage.removeItem('locked');
sessionStorage.removeItem('unlocked'); sessionStorage.removeItem('unlocked');
} }
const privateKey = deriveImportsEncryptionKey(seed); setUnlockedMnemonicAndSeed(mnemonic, seed);
setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey);
} }
export async function loadMnemonicAndSeed(password, stayLoggedIn) { export async function loadMnemonicAndSeed(password, stayLoggedIn) {
@ -102,8 +91,7 @@ export async function loadMnemonicAndSeed(password, stayLoggedIn) {
if (stayLoggedIn) { if (stayLoggedIn) {
sessionStorage.setItem('unlocked', decodedPlaintext); sessionStorage.setItem('unlocked', decodedPlaintext);
} }
const privateKey = deriveImportsEncryptionKey(seed); setUnlockedMnemonicAndSeed(mnemonic, seed);
setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey);
return { mnemonic, seed }; return { mnemonic, seed };
} }
@ -121,12 +109,5 @@ async function deriveEncryptionKey(password, salt, iterations, digest) {
} }
export function lockWallet() { export function lockWallet() {
setUnlockedMnemonicAndSeed(null, null, null); setUnlockedMnemonicAndSeed(null, null);
}
// Returns the 32 byte key used to encrypt imported private keys.
function deriveImportsEncryptionKey(seed) {
// SLIP16 derivation path.
return bip32.fromSeed(Buffer.from(seed, 'hex')).derivePath("m/10016'/0")
.privateKey;
} }

View File

@ -1,12 +1,6 @@
import React, { useContext, useMemo } from 'react'; import React, { useContext, useMemo } from 'react';
import * as bip32 from 'bip32'; import * as bip32 from 'bip32';
import * as bs58 from 'bs58'; import { Account, SystemProgram, Transaction } from '@solana/web3.js';
import {
Account,
SystemProgram,
Transaction,
PublicKey,
} from '@solana/web3.js';
import nacl from 'tweetnacl'; import nacl from 'tweetnacl';
import { import {
setInitialAccountInfo, setInitialAccountInfo,
@ -30,15 +24,12 @@ import { useTokenName } from './tokens/names';
import { refreshCache, useAsyncData } from './fetch-loop'; import { refreshCache, useAsyncData } from './fetch-loop';
import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed'; import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed';
const DEFAULT_WALLET_SELECTOR = {
walletIndex: 0,
importedPubkey: undefined,
};
export class Wallet { export class Wallet {
constructor(connection, account) { constructor(connection, seed, walletIndex = 0) {
this.connection = connection; this.connection = connection;
this.account = account; this.seed = seed;
this.walletIndex = walletIndex;
this.account = Wallet.getAccountFromSeed(this.seed, this.walletIndex);
} }
static getAccountFromSeed(seed, walletIndex, accountIndex = 0) { static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
@ -122,63 +113,19 @@ const WalletContext = React.createContext(null);
export function WalletProvider({ children }) { export function WalletProvider({ children }) {
useListener(walletSeedChanged, 'change'); useListener(walletSeedChanged, 'change');
const { mnemonic, seed, importsEncryptionKey } = getUnlockedMnemonicAndSeed(); const { mnemonic, seed } = getUnlockedMnemonicAndSeed();
const connection = useConnection(); const connection = useConnection();
const [walletIndex, setWalletIndex] = useLocalStorageState('walletIndex', 0);
// `privateKeyImports` are accounts imported *in addition* to HD wallets const wallet = useMemo(
const [privateKeyImports, setPrivateKeyImports] = useLocalStorageState( () =>
'walletPrivateKeyImports', seed
{}, ? new Wallet(connection, Buffer.from(seed, 'hex'), walletIndex)
: null,
[connection, seed, walletIndex],
); );
// `walletSelector` identifies which wallet to use.
const [walletSelector, setWalletSelector] = useLocalStorageState(
'walletSelector',
DEFAULT_WALLET_SELECTOR,
);
const wallet = useMemo(() => {
if (!seed) {
return null;
}
const account =
walletSelector.walletIndex !== undefined
? Wallet.getAccountFromSeed(
Buffer.from(seed, 'hex'),
walletSelector.walletIndex,
)
: new Account(
(() => {
const { nonce, ciphertext } = privateKeyImports[
walletSelector.importedPubkey
];
return nacl.secretbox.open(
bs58.decode(ciphertext),
bs58.decode(nonce),
importsEncryptionKey,
);
})(),
);
return new Wallet(connection, account);
}, [
connection,
seed,
walletSelector,
privateKeyImports,
importsEncryptionKey,
]);
return ( return (
<WalletContext.Provider <WalletContext.Provider
value={{ value={{ wallet, walletIndex, setWalletIndex, seed, mnemonic }}
wallet,
seed,
mnemonic,
importsEncryptionKey,
walletSelector,
setWalletSelector,
privateKeyImports,
setPrivateKeyImports,
}}
> >
{children} {children}
</WalletContext.Provider> </WalletContext.Provider>
@ -297,66 +244,34 @@ export function useBalanceInfo(publicKey) {
} }
export function useWalletSelector() { export function useWalletSelector() {
const { const { walletIndex, setWalletIndex, seed } = useContext(WalletContext);
seed,
importsEncryptionKey,
walletSelector,
setWalletSelector,
privateKeyImports,
setPrivateKeyImports,
} = useContext(WalletContext);
// `walletCount` is the number of HD wallets.
const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1); const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1);
function selectWallet(walletIndex, name) {
function addAccount({ name, importedAccount }) { if (walletIndex >= walletCount) {
if (importedAccount === undefined) { name && localStorage.setItem(`name${walletIndex}`, name);
name && localStorage.setItem(`name${walletCount}`, name); setWalletCount(walletIndex + 1);
setWalletCount(walletCount + 1);
} else {
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const plaintext = importedAccount.secretKey;
const ciphertext = nacl.secretbox(plaintext, nonce, importsEncryptionKey);
// `useLocalStorageState` requires a new object.
let newPrivateKeyImports = { ...privateKeyImports };
newPrivateKeyImports[importedAccount.publicKey.toString()] = {
name,
ciphertext: bs58.encode(ciphertext),
nonce: bs58.encode(nonce),
};
setPrivateKeyImports(newPrivateKeyImports);
} }
setWalletIndex(walletIndex);
} }
const accounts = useMemo(() => { const accounts = useMemo(() => {
if (!seed) { if (!seed) {
return []; return [];
} }
const seedBuffer = Buffer.from(seed, 'hex'); const seedBuffer = Buffer.from(seed, 'hex');
const derivedAccounts = [...Array(walletCount).keys()].map((idx) => { return [...Array(walletCount).keys()].map((walletIndex) => {
let address = Wallet.getAccountFromSeed(seedBuffer, idx).publicKey; let address = Wallet.getAccountFromSeed(seedBuffer, walletIndex)
let name = localStorage.getItem(`name${idx}`); .publicKey;
return { let name = localStorage.getItem(`name${walletIndex}`);
selector: { walletIndex: idx, importedPubkey: undefined }, return { index: walletIndex, address, name };
isSelected: walletSelector.walletIndex === idx,
address,
name: idx === 0 ? 'Main account' : name || `Account ${idx}`,
};
}); });
}, [seed, walletCount]);
const importedAccounts = Object.keys(privateKeyImports).map((pubkey) => { return { accounts, walletIndex, setWalletIndex: selectWallet };
const { name } = privateKeyImports[pubkey]; }
return {
selector: { walletIndex: undefined, importedPubkey: pubkey }, export async function mnemonicToSecretKey(mnemonic) {
address: new PublicKey(bs58.decode(pubkey)), const { mnemonicToSeed } = await import('bip39');
name: `${name} (imported)`, // TODO: do this in the Component with styling. const rootSeed = Buffer.from(await mnemonicToSeed(mnemonic), 'hex');
isSelected: walletSelector.importedPubkey === pubkey, const derivedSeed = bip32.fromSeed(rootSeed).derivePath("m/501'/0'/0/0")
}; .privateKey;
}); return nacl.sign.keyPair.fromSeed(derivedSeed).secretKey;
return derivedAccounts.concat(importedAccounts);
}, [seed, walletCount, walletSelector, privateKeyImports]);
return { accounts, setWalletSelector, addAccount };
} }