diff --git a/src/components/AddAccountDialog.js b/src/components/AddAccountDialog.js index 07aa6d4..7d03712 100644 --- a/src/components/AddAccountDialog.js +++ b/src/components/AddAccountDialog.js @@ -4,83 +4,35 @@ import Button from '@material-ui/core/Button'; import DialogTitle from '@material-ui/core/DialogTitle'; import DialogContent from '@material-ui/core/DialogContent'; 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'; export default function AddAccountDialog({ open, onAdd, onClose }) { 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 ( onAdd({ name, importedAccount })} + onSubmit={() => onAdd(name)} fullWidth > Add account -
- setName(e.target.value.trim())} - /> - - setIsImport(!isImport)} - /> - } - label="Import private key" - /> - - {isImport && ( - setPrivateKey(e.target.value.trim())} - /> - )} -
+ setName(e.target.value.trim())} + />
-
); } - -function decodeAccount(privateKey) { - try { - const a = new Account(bs58.decode(privateKey)); - return a; - } catch (_) { - return undefined; - } -} diff --git a/src/components/NavigationFrame.js b/src/components/NavigationFrame.js index 46a4963..7c32b48 100644 --- a/src/components/NavigationFrame.js +++ b/src/components/NavigationFrame.js @@ -126,7 +126,7 @@ function NetworkSelector() { } function WalletSelector() { - const { accounts, setWalletSelector, addAccount } = useWalletSelector(); + const { accounts, walletIndex, setWalletIndex } = useWalletSelector(); const [anchorEl, setAnchorEl] = useState(null); const [addAccountOpen, setAddAccountOpen] = useState(false); const classes = useStyles(); @@ -140,14 +140,8 @@ function WalletSelector() { setAddAccountOpen(false)} - onAdd={({ name, importedAccount }) => { - addAccount({ name, importedAccount }); - setWalletSelector({ - walletIndex: importedAccount ? undefined : accounts.length, - importedPubkey: importedAccount - ? importedAccount.publicKey.toString() - : undefined, - }); + onAdd={(name) => { + setWalletIndex(accounts.length, name); setAddAccountOpen(false); }} /> @@ -177,21 +171,23 @@ function WalletSelector() { }} getContentAnchorEl={null} > - {accounts.map(({ isSelected, selector, address, name, label }) => ( + {accounts.map(({ index, address, name }) => ( { setAnchorEl(null); - setWalletSelector(selector); + setWalletIndex(index); }} - selected={isSelected} + selected={index === walletIndex} component="div" > - {isSelected ? : null} + {index === walletIndex ? : null}
- {name} + + {index === 0 ? 'Main account' : name || `Account ${index}`} + {address.toBase58()} @@ -208,7 +204,7 @@ function WalletSelector() { - Add Account + Create Account diff --git a/src/components/SendDialog.js b/src/components/SendDialog.js index ffa0d3d..cc279a4 100644 --- a/src/components/SendDialog.js +++ b/src/components/SendDialog.js @@ -28,10 +28,7 @@ import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import { useAsyncData } from '../utils/fetch-loop'; import CircularProgress from '@material-ui/core/CircularProgress'; -import { - TOKEN_PROGRAM_ID, - WRAPPED_SOL_MINT, -} from '../utils/tokens/instructions'; +import {TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT} from '../utils/tokens/instructions'; import { parseTokenAccountData } from '../utils/tokens/data'; const WUSDC_MINT = new PublicKey( @@ -137,10 +134,9 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) { } function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) { - const defaultAddressHelperText = - !balanceInfo.mint || balanceInfo.mint.equals(WRAPPED_SOL_MINT) - ? 'Enter Solana Address' - : 'Enter SPL token or Solana address'; + const defaultAddressHelperText = !balanceInfo.mint || balanceInfo.mint.equals(WRAPPED_SOL_MINT) ? + 'Enter Solana Address' : + 'Enter SPL token or Solana address'; const wallet = useWallet(); const [sendTransaction, sending] = useSendTransaction(); const [addressHelperText, setAddressHelperText] = useState( diff --git a/src/utils/wallet-seed.js b/src/utils/wallet-seed.js index c6e19be..7f36682 100644 --- a/src/utils/wallet-seed.js +++ b/src/utils/wallet-seed.js @@ -1,6 +1,5 @@ import { pbkdf2 } from 'crypto'; import { randomBytes, secretbox } from 'tweetnacl'; -import * as bip32 from 'bip32'; import bs58 from 'bs58'; import { EventEmitter } from 'events'; @@ -20,20 +19,11 @@ export async function mnemonicToSeed(mnemonic) { return Buffer.from(seed).toString('hex'); } -let unlockedMnemonicAndSeed = (() => { - const stored = JSON.parse( - sessionStorage.getItem('unlocked') || - localStorage.getItem('unlocked') || - 'null', - ); - if (stored === null) { - return { mnemonic: null, seed: null, importsEncryptionKey: null }; - } - return { - importsEncryptionKey: deriveImportsEncryptionKey(stored.seed), - ...stored, - }; -})(); +let unlockedMnemonicAndSeed = JSON.parse( + sessionStorage.getItem('unlocked') || + localStorage.getItem('unlocked') || + 'null', +) || { mnemonic: null, seed: null }; export const walletSeedChanged = new EventEmitter(); export function getUnlockedMnemonicAndSeed() { @@ -44,8 +34,8 @@ export function hasLockedMnemonicAndSeed() { return !!localStorage.getItem('locked'); } -function setUnlockedMnemonicAndSeed(mnemonic, seed, importsEncryptionKey) { - unlockedMnemonicAndSeed = { mnemonic, seed, importsEncryptionKey }; +function setUnlockedMnemonicAndSeed(mnemonic, seed) { + unlockedMnemonicAndSeed = { mnemonic, seed }; walletSeedChanged.emit('change', unlockedMnemonicAndSeed); } @@ -77,8 +67,7 @@ export async function storeMnemonicAndSeed(mnemonic, seed, password) { localStorage.removeItem('locked'); sessionStorage.removeItem('unlocked'); } - const privateKey = deriveImportsEncryptionKey(seed); - setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey); + setUnlockedMnemonicAndSeed(mnemonic, seed); } export async function loadMnemonicAndSeed(password, stayLoggedIn) { @@ -102,8 +91,7 @@ export async function loadMnemonicAndSeed(password, stayLoggedIn) { if (stayLoggedIn) { sessionStorage.setItem('unlocked', decodedPlaintext); } - const privateKey = deriveImportsEncryptionKey(seed); - setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey); + setUnlockedMnemonicAndSeed(mnemonic, seed); return { mnemonic, seed }; } @@ -121,12 +109,5 @@ async function deriveEncryptionKey(password, salt, iterations, digest) { } export function lockWallet() { - setUnlockedMnemonicAndSeed(null, 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; + setUnlockedMnemonicAndSeed(null, null); } diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 171435e..98f0c9b 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -1,12 +1,6 @@ import React, { useContext, useMemo } from 'react'; import * as bip32 from 'bip32'; -import * as bs58 from 'bs58'; -import { - Account, - SystemProgram, - Transaction, - PublicKey, -} from '@solana/web3.js'; +import { Account, SystemProgram, Transaction } from '@solana/web3.js'; import nacl from 'tweetnacl'; import { setInitialAccountInfo, @@ -30,15 +24,12 @@ import { useTokenName } from './tokens/names'; import { refreshCache, useAsyncData } from './fetch-loop'; import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed'; -const DEFAULT_WALLET_SELECTOR = { - walletIndex: 0, - importedPubkey: undefined, -}; - export class Wallet { - constructor(connection, account) { + constructor(connection, seed, walletIndex = 0) { 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) { @@ -122,63 +113,19 @@ const WalletContext = React.createContext(null); export function WalletProvider({ children }) { useListener(walletSeedChanged, 'change'); - const { mnemonic, seed, importsEncryptionKey } = getUnlockedMnemonicAndSeed(); + const { mnemonic, seed } = getUnlockedMnemonicAndSeed(); const connection = useConnection(); - - // `privateKeyImports` are accounts imported *in addition* to HD wallets - const [privateKeyImports, setPrivateKeyImports] = useLocalStorageState( - 'walletPrivateKeyImports', - {}, + const [walletIndex, setWalletIndex] = useLocalStorageState('walletIndex', 0); + const wallet = useMemo( + () => + 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 ( {children} @@ -297,66 +244,34 @@ export function useBalanceInfo(publicKey) { } export function useWalletSelector() { - const { - seed, - importsEncryptionKey, - walletSelector, - setWalletSelector, - privateKeyImports, - setPrivateKeyImports, - } = useContext(WalletContext); - - // `walletCount` is the number of HD wallets. + const { walletIndex, setWalletIndex, seed } = useContext(WalletContext); const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1); - - function addAccount({ name, importedAccount }) { - if (importedAccount === undefined) { - name && localStorage.setItem(`name${walletCount}`, name); - 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); + function selectWallet(walletIndex, name) { + if (walletIndex >= walletCount) { + name && localStorage.setItem(`name${walletIndex}`, name); + setWalletCount(walletIndex + 1); } + setWalletIndex(walletIndex); } - const accounts = useMemo(() => { if (!seed) { return []; } - const seedBuffer = Buffer.from(seed, 'hex'); - const derivedAccounts = [...Array(walletCount).keys()].map((idx) => { - let address = Wallet.getAccountFromSeed(seedBuffer, idx).publicKey; - let name = localStorage.getItem(`name${idx}`); - return { - selector: { walletIndex: idx, importedPubkey: undefined }, - isSelected: walletSelector.walletIndex === idx, - address, - name: idx === 0 ? 'Main account' : name || `Account ${idx}`, - }; + return [...Array(walletCount).keys()].map((walletIndex) => { + let address = Wallet.getAccountFromSeed(seedBuffer, walletIndex) + .publicKey; + let name = localStorage.getItem(`name${walletIndex}`); + return { index: walletIndex, address, name }; }); - - const importedAccounts = Object.keys(privateKeyImports).map((pubkey) => { - const { name } = privateKeyImports[pubkey]; - return { - selector: { walletIndex: undefined, importedPubkey: pubkey }, - address: new PublicKey(bs58.decode(pubkey)), - name: `${name} (imported)`, // TODO: do this in the Component with styling. - isSelected: walletSelector.importedPubkey === pubkey, - }; - }); - - return derivedAccounts.concat(importedAccounts); - }, [seed, walletCount, walletSelector, privateKeyImports]); - - return { accounts, setWalletSelector, addAccount }; + }, [seed, walletCount]); + return { accounts, walletIndex, setWalletIndex: selectWallet }; +} + +export async function mnemonicToSecretKey(mnemonic) { + const { mnemonicToSeed } = await import('bip39'); + const rootSeed = Buffer.from(await mnemonicToSeed(mnemonic), 'hex'); + const derivedSeed = bip32.fromSeed(rootSeed).derivePath("m/501'/0'/0/0") + .privateKey; + return nacl.sign.keyPair.fromSeed(derivedSeed).secretKey; }