diff --git a/package.json b/package.json index db79420..e4017be 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@ledgerhq/hw-transport-webusb": "^5.45.0", "@material-ui/core": "^4.11.2", "@material-ui/icons": "^4.11.2", - "@project-serum/serum": "^0.13.24", + "@project-serum/serum": "^0.13.33", "@solana/spl-token-registry": "^0.2.1", "@solana/web3.js": "^0.87.2", "@testing-library/jest-dom": "^5.11.6", @@ -52,7 +52,9 @@ "es6": true, "webextensions": true }, - "extends": ["react-app"] + "extends": [ + "react-app" + ] }, "jest": { "transformIgnorePatterns": [ 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..f5283ca 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -23,7 +23,6 @@ import { abbreviateAddress, useIsExtensionWidth } from '../utils/utils'; import Button from '@material-ui/core/Button'; import SendIcon from '@material-ui/icons/Send'; import ReceiveIcon from '@material-ui/icons/WorkOutline'; -import DeleteIcon from '@material-ui/icons/Delete'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import AddIcon from '@material-ui/icons/Add'; @@ -32,9 +31,7 @@ import IconButton from '@material-ui/core/IconButton'; import InfoIcon from '@material-ui/icons/InfoOutlined'; import Tooltip from '@material-ui/core/Tooltip'; import EditIcon from '@material-ui/icons/Edit'; -import MergeType from '@material-ui/icons/MergeType'; import SortIcon from '@material-ui/icons/Sort'; -import FingerprintIcon from '@material-ui/icons/Fingerprint'; import AddTokenDialog from './AddTokenDialog'; import ExportAccountDialog from './ExportAccountDialog'; import SendDialog from './SendDialog'; @@ -212,14 +209,6 @@ export default function BalancesList() { )} - - setShowMergeAccounts(true)} - > - - - - + { @@ -363,7 +352,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) { @@ -394,47 +390,39 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { } } - const isAssociatedToken = (() => { - if ( - wallet && - wallet.publicKey && - mint && - associatedTokensCache[wallet.publicKey.toString()] - ) { - let acc = - associatedTokensCache[wallet.publicKey.toString()][mint.toString()]; - if (acc && acc.equals(publicKey)) { - return true; + // undefined => not loaded. + let isAssociatedToken = mint ? undefined : false; + if ( + wallet && + wallet.publicKey && + mint && + associatedTokensCache[wallet.publicKey.toString()] + ) { + let acc = + associatedTokensCache[wallet.publicKey.toString()][mint.toString()]; + if (acc) { + if (acc.equals(publicKey)) { + isAssociatedToken = true; + } else { + isAssociatedToken = false; } } - return false; - })(); + } - const subtitle = isExtensionWidth ? undefined : ( -
- {isAssociatedToken && ( + const subtitle = + isExtensionWidth || !publicKey.equals(balanceInfo.owner) ? undefined : ( +
- + {publicKey.toBase58()}
- )} -
- {publicKey.toBase58()}
-
- ); + ); const usdValue = price === undefined // Not yet loaded. @@ -450,7 +438,12 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { <> expandable && setOpen((open) => !open)}> - +
{expandable ? open ? : : <>} - - - + {expandable && ( + + + + )} ); } -function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { +function BalanceListItemDetails({ + publicKey, + serumMarkets, + balanceInfo, + isAssociatedToken, +}) { const urlSuffix = useSolanaExplorerUrlSuffix(); const classes = useStyles(); const [sendDialogOpen, setSendDialogOpen] = useState(false); @@ -529,7 +530,7 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { return ; } - let { mint, tokenName, tokenSymbol, owner, amount } = balanceInfo; + let { mint, tokenName, tokenSymbol, owner } = balanceInfo; // Only show the export UI for the native SOL coin. const exportNeedsDisplay = @@ -540,12 +541,9 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { ? serumMarkets[tokenSymbol.toUpperCase()].publicKey : undefined : undefined; - + const isSolAddress = publicKey.equals(owner); const additionalInfo = isExtensionWidth ? undefined : ( <> - - Deposit Address: {publicKey.toBase58()} - Token Name: {tokenName ?? 'Unknown'} @@ -557,6 +555,17 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { Token Address: {mint.toBase58()} ) : null} + {!isSolAddress && ( + + {isAssociatedToken ? 'Associated' : ''} Token Metadata:{' '} + {publicKey.toBase58()} + + )} + {!isSolAddress && isAssociatedToken === false && ( +
+ This is an auxiliary token account. +
+ )}
@@ -646,17 +655,6 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { > Send - {mint && amount === 0 ? ( - - ) : null}
{additionalInfo}
@@ -672,6 +670,7 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { balanceInfo={balanceInfo} publicKey={publicKey} swapInfo={swapInfo} + isAssociatedToken={isAssociatedToken} /> ); } - + const displaySolAddress = publicKey.equals(owner) || isAssociatedToken; + const depositAddressStr = displaySolAddress + ? owner.toBase58() + : publicKey.toBase58(); return ( - + Deposit {tokenName ?? mint.toBase58()} {tokenSymbol ? ` (${tokenSymbol})` : null} @@ -84,20 +88,20 @@ export default function DepositDialog({ {tab === 0 ? ( <> - {publicKey.equals(owner) ? ( - - This address can only be used to receive SOL. Do not send other - tokens to this address. - - ) : ( + {!displaySolAddress && isAssociatedToken === false ? ( This address can only be used to receive{' '} {tokenSymbol ?? abbreviateAddress(mint)}. Do not send SOL to this address. + ) : ( + + This address can be used to receive{' '} + {tokenSymbol ?? abbreviateAddress(mint)}. + )} ) : ( <> - Are you sure you want to merge accounts? + Are you sure you want to merge tokens? WARNING: This action may break apps that depend on your - existing accounts. + existing token accounts. Merging sends all tokens to{' '} diff --git a/src/components/NavigationFrame.js b/src/components/NavigationFrame.js index 7d1a517..e996566 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,13 @@ function NetworkSelector() { } function WalletSelector() { - const { accounts, setWalletSelector, addAccount } = useWalletSelector(); + const { + accounts, + hardwareWalletAccount, + setHardwareWalletAccount, + setWalletSelector, + addAccount, + } = useWalletSelector(); const [anchorEl, setAnchorEl] = useState(null); const [addAccountOpen, setAddAccountOpen] = useState(false); const [ @@ -251,22 +259,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, }); }} /> @@ -319,27 +333,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 +423,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 ( - - - - ); - })} -
+