From 39c9ac7277b9a211c5a344da2675435ebeaa5239 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Fri, 2 Apr 2021 10:49:30 -0700 Subject: [PATCH 1/5] Ledger path parameterization (#158) --- src/components/AddHarwareWalletDialog.js | 229 +++++++++++++++++------ src/components/BalancesList.js | 32 +++- src/components/NavigationFrame.js | 92 ++++++--- src/pages/LoginPage.js | 128 ++++++++----- src/utils/wallet.js | 72 ++++--- src/utils/walletProvider/ledger-core.js | 59 ++++-- src/utils/walletProvider/ledger.js | 27 ++- src/utils/walletProvider/localStorage.js | 1 + 8 files changed, 444 insertions(+), 196 deletions(-) diff --git a/src/components/AddHarwareWalletDialog.js b/src/components/AddHarwareWalletDialog.js index cf09a87..34ca24f 100644 --- a/src/components/AddHarwareWalletDialog.js +++ b/src/components/AddHarwareWalletDialog.js @@ -1,53 +1,102 @@ import React, { useEffect, useState } from 'react'; +import Typography from '@material-ui/core/Typography'; import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; import DialogTitle from '@material-ui/core/DialogTitle'; import DialogContent from '@material-ui/core/DialogContent'; +import Card from '@material-ui/core/Card'; import DialogForm from './DialogForm'; import { LedgerWalletProvider } from '../utils/walletProvider/ledger'; +import { + AccountsSelector, + DerivationPathMenuItem, + toDerivationPath, +} from '../pages/LoginPage.js'; import CircularProgress from '@material-ui/core/CircularProgress'; import { useSnackbar } from 'notistack'; +const AddHardwareView = { + Splash: 0, + Accounts: 1, + Confirm: 2, +}; + export default function AddHardwareWalletDialog({ open, onAdd, onClose }) { - const [pubKey, setPubKey] = useState(); - const { enqueueSnackbar } = useSnackbar(); - - useEffect(() => { - (async () => { - if (open) { - try { - const provider = new LedgerWalletProvider(); - await provider.init(); - setPubKey(provider.publicKey); - } catch (err) { - console.log( - `received error when attempting to connect ledger: ${err}`, - ); - if (err.statusCode === 0x6804) { - enqueueSnackbar('Unlock ledger device', { variant: 'error' }); - } - setPubKey(undefined); - onClose(); - } - } - })(); - }, [open, onClose, enqueueSnackbar]); - + const [view, setView] = useState(AddHardwareView.Splash); + const [hardwareAccount, setHardwareAccount] = useState(null); return ( - {}} - onClose={() => { - setPubKey(undefined); - onClose(); - }} - onSubmit={() => { - setPubKey(undefined); - onAdd(pubKey); - onClose(); - }} - fullWidth - > + {}} fullWidth> + {view === AddHardwareView.Splash ? ( + setView(AddHardwareView.Accounts)} + /> + ) : view === AddHardwareView.Accounts ? ( + { + setHardwareAccount(account); + setView(AddHardwareView.Confirm); + }} + open={open} + onClose={onClose} + /> + ) : ( + { + onAdd(hardwareAccount); + onClose(); + setView(AddHardwareView.Splash); + }} + onBack={() => { + setView(AddHardwareView.Accounts); + }} + /> + )} + + ); +} + +function ConfirmHardwareWallet({ account, onDone, onBack }) { + const [didConfirm, setDidConfirm] = useState(false); + useEffect(() => { + if (!didConfirm) { + account.provider + .confirmPublicKey() + .then(() => setDidConfirm(true)) + .catch((err) => { + console.error('Error confirming', err); + onBack(); + }); + } + }); + return ( + <> + Confirm your wallet address + +
+ + Check your ledger and confirm the address displayed is the address + chosen. Then click "done". + + {account.publicKey.toString()} +
+
+ + + + + + ); +} + +function AddHardwareWalletSplash({ onContinue, onClose }) { + return ( + <> Add hardware wallet
- {pubKey ? ( - <> - Hardware wallet detected: -
{pubKey.toString()}
- - ) : ( - <> - Connect your ledger and open the Solana application - - - )} + + Connect your ledger and open the Solana application. When you are + ready, click "continue". +
- - -
+ + ); +} + +function LedgerAccounts({ onContinue, onClose, open }) { + const [dPathMenuItem, setDPathMenuItem] = useState( + DerivationPathMenuItem.Bip44Root, + ); + const { enqueueSnackbar } = useSnackbar(); + const [accounts, setAccounts] = useState(null); + const onClick = (provider) => { + onContinue({ + provider, + publicKey: provider.pubKey, + derivationPath: provider.derivationPath, + account: provider.account, + change: provider.change, + }); + }; + useEffect(() => { + if (open) { + const fetch = async () => { + let accounts = []; + if (dPathMenuItem === DerivationPathMenuItem.Bip44Root) { + let provider = new LedgerWalletProvider({ + derivationPath: toDerivationPath(dPathMenuItem), + }); + accounts.push(await provider.init()); + } else { + setAccounts(null); + // Loading in parallel makes the ledger upset. So do it serially. + for (let k = 0; k < 10; k += 1) { + let provider = new LedgerWalletProvider({ + derivationPath: toDerivationPath(dPathMenuItem), + account: k, + }); + accounts.push(await provider.init()); + } + } + setAccounts(accounts); + }; + fetch().catch((err) => { + console.log(`received error when attempting to connect ledger: ${err}`); + if (err && err.statusCode === 0x6804) { + enqueueSnackbar('Unlock ledger device', { variant: 'error' }); + } + onClose(); + }); + } + }, [dPathMenuItem, enqueueSnackbar, open, onClose]); + return ( + + {accounts === null ? ( +
+ + Loading accounts from your hardware wallet + + +
+ ) : ( + + )} +
); } diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index 834e09c..cd2cad3 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -363,7 +363,14 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { return ; } - let { amount, decimals, mint, tokenName, tokenSymbol, tokenLogoUri } = balanceInfo; + let { + amount, + decimals, + mint, + tokenName, + tokenSymbol, + tokenLogoUri, + } = balanceInfo; tokenName = tokenName ?? abbreviateAddress(mint); let displayName; if (isExtensionWidth) { @@ -450,7 +457,12 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { <> expandable && setOpen((open) => !open)}> - +
{expandable ? open ? : : <>} - - - + {expandable && ( + + + + )} ); } diff --git a/src/components/NavigationFrame.js b/src/components/NavigationFrame.js index 7d1a517..852e470 100644 --- a/src/components/NavigationFrame.js +++ b/src/components/NavigationFrame.js @@ -184,7 +184,7 @@ function ConnectionsButton() { function NetworkSelector() { const { endpoint, setEndpoint } = useConnectionConfig(); - const cluster = useMemo(() => clusterForEndpoint(endpoint), [endpoint]) + const cluster = useMemo(() => clusterForEndpoint(endpoint), [endpoint]); const [anchorEl, setAnchorEl] = useState(null); const classes = useStyles(); @@ -226,7 +226,9 @@ function NetworkSelector() { selected={cluster.apiUrl === endpoint} > - {cluster.apiUrl === endpoint ? : null} + {cluster.apiUrl === endpoint ? ( + + ) : null} {cluster.apiUrl} @@ -237,7 +239,12 @@ function NetworkSelector() { } function WalletSelector() { - const { accounts, setWalletSelector, addAccount } = useWalletSelector(); + const { + accounts, + hardwareWalletAccount, + setHardwareWalletAccount, + setWalletSelector, + } = useWalletSelector(); const [anchorEl, setAnchorEl] = useState(null); const [addAccountOpen, setAddAccountOpen] = useState(false); const [ @@ -251,22 +258,28 @@ function WalletSelector() { if (accounts.length === 0) { return null; } - return ( <> setAddHardwareWalletDialogOpen(false)} - onAdd={(pubKey) => { - addAccount({ + onAdd={({ publicKey, derivationPath, account, change }) => { + setHardwareWalletAccount({ name: 'Hardware wallet', - importedAccount: pubKey.toString(), + publicKey, + importedAccount: publicKey.toString(), ledger: true, + derivationPath, + account, + change, }); setWalletSelector({ walletIndex: undefined, - importedPubkey: pubKey.toString(), + importedPubkey: publicKey.toString(), ledger: true, + derivationPath, + account, + change, }); }} /> @@ -274,7 +287,6 @@ function WalletSelector() { open={addAccountOpen} onClose={() => setAddAccountOpen(false)} onAdd={({ name, importedAccount }) => { - addAccount({ name, importedAccount }); setWalletSelector({ walletIndex: importedAccount ? undefined : accounts.length, importedPubkey: importedAccount @@ -319,27 +331,25 @@ function WalletSelector() { }} getContentAnchorEl={null} > - {accounts.map(({ isSelected, selector, address, name, label }) => ( - { - setAnchorEl(null); - setWalletSelector(selector); - }} - selected={isSelected} - component="div" - > - - {isSelected ? : null} - -
- {name} - - {address.toBase58()} - -
-
+ {accounts.map((account) => ( + ))} + {hardwareWalletAccount && ( + <> + + + + )} setAddHardwareWalletDialogOpen(true)}> @@ -411,3 +421,27 @@ function Footer() { ); } + +function AccountListItem({ account, classes, setAnchorEl, setWalletSelector }) { + return ( + { + setAnchorEl(null); + setWalletSelector(account.selector); + }} + selected={account.isSelected} + component="div" + > + + {account.isSelected ? : null} + +
+ {account.name} + + {account.address.toBase58()} + +
+
+ ); +} diff --git a/src/pages/LoginPage.js b/src/pages/LoginPage.js index b058d41..1b38fa8 100644 --- a/src/pages/LoginPage.js +++ b/src/pages/LoginPage.js @@ -10,7 +10,6 @@ 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'; @@ -276,6 +275,11 @@ function RestoreWalletForm({ goBack }) { Restore your wallet using your twelve or twenty-four seed words. Note that this will delete any existing wallet on this device. +
+ + Do not enter your hardware wallet seedphrase here. Hardware + wallets can be optionally connected after a web wallet is created. + { return getAccountFromSeed( Buffer.from(seed, 'hex'), @@ -356,52 +358,12 @@ function DerivedAccounts({ goBack, mnemonic, seed, password }) { return ( - -
- - Derivable Accounts - - - - -
- {accounts.map((acc) => { - return ( - - - - ); - })} -
+ - {mint && amount === 0 ? ( - - ) : null}
{additionalInfo}