Merge branch 'master' of github.com:project-serum/spl-token-wallet
This commit is contained in:
commit
63ae7bd130
|
@ -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": [
|
||||
|
|
|
@ -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 (
|
||||
<DialogForm
|
||||
open={open}
|
||||
onEnter={() => {}}
|
||||
onClose={() => {
|
||||
setPubKey(undefined);
|
||||
onClose();
|
||||
}}
|
||||
onSubmit={() => {
|
||||
setPubKey(undefined);
|
||||
onAdd(pubKey);
|
||||
onClose();
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<DialogForm onClose={onClose} open={open} onEnter={() => {}} fullWidth>
|
||||
{view === AddHardwareView.Splash ? (
|
||||
<AddHardwareWalletSplash
|
||||
onClose={onClose}
|
||||
onContinue={() => setView(AddHardwareView.Accounts)}
|
||||
/>
|
||||
) : view === AddHardwareView.Accounts ? (
|
||||
<LedgerAccounts
|
||||
onContinue={(account) => {
|
||||
setHardwareAccount(account);
|
||||
setView(AddHardwareView.Confirm);
|
||||
}}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<ConfirmHardwareWallet
|
||||
account={hardwareAccount}
|
||||
onDone={() => {
|
||||
onAdd(hardwareAccount);
|
||||
onClose();
|
||||
setView(AddHardwareView.Splash);
|
||||
}}
|
||||
onBack={() => {
|
||||
setView(AddHardwareView.Accounts);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogForm>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<DialogTitle>Confirm your wallet address</DialogTitle>
|
||||
<DialogContent style={{ paddingTop: 16 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="fontWeightBold">
|
||||
Check your ledger and confirm the address displayed is the address
|
||||
chosen. Then click "done".
|
||||
</Typography>
|
||||
<Typography>{account.publicKey.toString()}</Typography>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="primary" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button color="primary" onClick={onDone} disabled={!didConfirm}>
|
||||
Done
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AddHardwareWalletSplash({ onContinue, onClose }) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>Add hardware wallet</DialogTitle>
|
||||
<DialogContent style={{ paddingTop: 16 }}>
|
||||
<div
|
||||
|
@ -56,32 +105,94 @@ export default function AddHardwareWalletDialog({ open, onAdd, onClose }) {
|
|||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{pubKey ? (
|
||||
<>
|
||||
<b>Hardware wallet detected:</b>
|
||||
<div>{pubKey.toString()}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<b>Connect your ledger and open the Solana application</b>
|
||||
<CircularProgress />
|
||||
</>
|
||||
)}
|
||||
<b>
|
||||
Connect your ledger and open the Solana application. When you are
|
||||
ready, click "continue".
|
||||
</b>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPubKey(undefined);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
<Button color="primary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" color="primary" disabled={!pubKey}>
|
||||
Add
|
||||
<Button color="primary" onClick={onContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card elevation={0}>
|
||||
{accounts === null ? (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<Typography align="center">
|
||||
Loading accounts from your hardware wallet
|
||||
</Typography>
|
||||
<CircularProgress
|
||||
style={{
|
||||
display: 'block',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<AccountsSelector
|
||||
showRoot={true}
|
||||
onClick={onClick}
|
||||
accounts={accounts}
|
||||
setDPathMenuItem={setDPathMenuItem}
|
||||
dPathMenuItem={dPathMenuItem}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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() {
|
|||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Merge Accounts" arrow>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => setShowMergeAccounts(true)}
|
||||
>
|
||||
<MergeType />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Add Token" arrow>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
|
@ -228,7 +217,7 @@ export default function BalancesList() {
|
|||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Sort Accounts" arrow>
|
||||
<Tooltip title="Sort Tokens" arrow>
|
||||
<IconButton
|
||||
size={iconSize}
|
||||
onClick={() => {
|
||||
|
@ -363,7 +352,14 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) {
|
|||
return <LoadingIndicator delay={0} />;
|
||||
}
|
||||
|
||||
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 : (
|
||||
<div style={{ display: 'flex', height: '20px', overflow: 'hidden' }}>
|
||||
{isAssociatedToken && (
|
||||
const subtitle =
|
||||
isExtensionWidth || !publicKey.equals(balanceInfo.owner) ? undefined : (
|
||||
<div style={{ display: 'flex', height: '20px', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
marginRight: '5px',
|
||||
}}
|
||||
>
|
||||
<FingerprintIcon style={{ width: '20px' }} />
|
||||
{publicKey.toBase58()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{publicKey.toBase58()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
||||
const usdValue =
|
||||
price === undefined // Not yet loaded.
|
||||
|
@ -450,7 +438,12 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) {
|
|||
<>
|
||||
<ListItem button onClick={() => expandable && setOpen((open) => !open)}>
|
||||
<ListItemIcon>
|
||||
<TokenIcon mint={mint} tokenName={tokenName} url={tokenLogoUri} size={28} />
|
||||
<TokenIcon
|
||||
mint={mint}
|
||||
tokenName={tokenName}
|
||||
url={tokenLogoUri}
|
||||
size={28}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<ListItemText
|
||||
|
@ -479,18 +472,26 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) {
|
|||
</div>
|
||||
{expandable ? open ? <ExpandLess /> : <ExpandMore /> : <></>}
|
||||
</ListItem>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<BalanceListItemDetails
|
||||
publicKey={publicKey}
|
||||
serumMarkets={serumMarkets}
|
||||
balanceInfo={balanceInfo}
|
||||
/>
|
||||
</Collapse>
|
||||
{expandable && (
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<BalanceListItemDetails
|
||||
isAssociatedToken={isAssociatedToken}
|
||||
publicKey={publicKey}
|
||||
serumMarkets={serumMarkets}
|
||||
balanceInfo={balanceInfo}
|
||||
/>
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 <LoadingIndicator delay={0} />;
|
||||
}
|
||||
|
||||
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 : (
|
||||
<>
|
||||
<Typography variant="body2" className={classes.address}>
|
||||
Deposit Address: {publicKey.toBase58()}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Token Name: {tokenName ?? 'Unknown'}
|
||||
</Typography>
|
||||
|
@ -557,6 +555,17 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) {
|
|||
Token Address: {mint.toBase58()}
|
||||
</Typography>
|
||||
) : null}
|
||||
{!isSolAddress && (
|
||||
<Typography variant="body2" className={classes.address}>
|
||||
{isAssociatedToken ? 'Associated' : ''} Token Metadata:{' '}
|
||||
{publicKey.toBase58()}
|
||||
</Typography>
|
||||
)}
|
||||
{!isSolAddress && isAssociatedToken === false && (
|
||||
<div style={{ display: 'flex' }}>
|
||||
This is an auxiliary token account.
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
|
@ -646,17 +655,6 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) {
|
|||
>
|
||||
Send
|
||||
</Button>
|
||||
{mint && amount === 0 ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => setCloseTokenAccountDialogOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{additionalInfo}
|
||||
</div>
|
||||
|
@ -672,6 +670,7 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) {
|
|||
balanceInfo={balanceInfo}
|
||||
publicKey={publicKey}
|
||||
swapInfo={swapInfo}
|
||||
isAssociatedToken={isAssociatedToken}
|
||||
/>
|
||||
<TokenInfoDialog
|
||||
open={tokenInfoDialogOpen}
|
||||
|
|
|
@ -36,6 +36,7 @@ export default function DepositDialog({
|
|||
publicKey,
|
||||
balanceInfo,
|
||||
swapInfo,
|
||||
isAssociatedToken,
|
||||
}) {
|
||||
const ethAccount = useEthAccount();
|
||||
const urlSuffix = useSolanaExplorerUrlSuffix();
|
||||
|
@ -66,9 +67,12 @@ export default function DepositDialog({
|
|||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
const displaySolAddress = publicKey.equals(owner) || isAssociatedToken;
|
||||
const depositAddressStr = displaySolAddress
|
||||
? owner.toBase58()
|
||||
: publicKey.toBase58();
|
||||
return (
|
||||
<DialogForm open={open} onClose={onClose}>
|
||||
<DialogForm open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
Deposit {tokenName ?? mint.toBase58()}
|
||||
{tokenSymbol ? ` (${tokenSymbol})` : null}
|
||||
|
@ -84,20 +88,20 @@ export default function DepositDialog({
|
|||
<DialogContent style={{ paddingTop: 16 }}>
|
||||
{tab === 0 ? (
|
||||
<>
|
||||
{publicKey.equals(owner) ? (
|
||||
<DialogContentText>
|
||||
This address can only be used to receive SOL. Do not send other
|
||||
tokens to this address.
|
||||
</DialogContentText>
|
||||
) : (
|
||||
{!displaySolAddress && isAssociatedToken === false ? (
|
||||
<DialogContentText>
|
||||
This address can only be used to receive{' '}
|
||||
{tokenSymbol ?? abbreviateAddress(mint)}. Do not send SOL to
|
||||
this address.
|
||||
</DialogContentText>
|
||||
) : (
|
||||
<DialogContentText>
|
||||
This address can be used to receive{' '}
|
||||
{tokenSymbol ?? abbreviateAddress(mint)}.
|
||||
</DialogContentText>
|
||||
)}
|
||||
<CopyableDisplay
|
||||
value={publicKey.toBase58()}
|
||||
value={depositAddressStr}
|
||||
label={'Deposit Address'}
|
||||
autoFocus
|
||||
qrCode
|
||||
|
@ -105,7 +109,7 @@ export default function DepositDialog({
|
|||
<DialogContentText variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
|
||||
`https://explorer.solana.com/account/${depositAddressStr}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
|
|
|
@ -168,11 +168,11 @@ export default function MergeAccountsDialog({ open, onClose }) {
|
|||
</DialogContent>
|
||||
) : (
|
||||
<>
|
||||
<DialogTitle>Are you sure you want to merge accounts?</DialogTitle>
|
||||
<DialogTitle>Are you sure you want to merge tokens?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<b>WARNING</b>: This action may break apps that depend on your
|
||||
existing accounts.
|
||||
existing token accounts.
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
Merging sends all tokens to{' '}
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
{cluster.apiUrl === endpoint ? <CheckIcon fontSize="small" /> : null}
|
||||
{cluster.apiUrl === endpoint ? (
|
||||
<CheckIcon fontSize="small" />
|
||||
) : null}
|
||||
</ListItemIcon>
|
||||
{cluster.apiUrl}
|
||||
</MenuItem>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<AddHardwareWalletDialog
|
||||
open={addHardwareWalletDialogOpen}
|
||||
onClose={() => 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 }) => (
|
||||
<MenuItem
|
||||
key={address.toBase58()}
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
setWalletSelector(selector);
|
||||
}}
|
||||
selected={isSelected}
|
||||
component="div"
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
{isSelected ? <CheckIcon fontSize="small" /> : null}
|
||||
</ListItemIcon>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography>{name}</Typography>
|
||||
<Typography color="textSecondary">
|
||||
{address.toBase58()}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
{accounts.map((account) => (
|
||||
<AccountListItem
|
||||
account={account}
|
||||
classes={classes}
|
||||
setAnchorEl={setAnchorEl}
|
||||
setWalletSelector={setWalletSelector}
|
||||
/>
|
||||
))}
|
||||
{hardwareWalletAccount && (
|
||||
<>
|
||||
<Divider />
|
||||
<AccountListItem
|
||||
account={hardwareWalletAccount}
|
||||
classes={classes}
|
||||
setAnchorEl={setAnchorEl}
|
||||
setWalletSelector={setWalletSelector}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<MenuItem onClick={() => setAddHardwareWalletDialogOpen(true)}>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
|
@ -411,3 +423,27 @@ function Footer() {
|
|||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountListItem({ account, classes, setAnchorEl, setWalletSelector }) {
|
||||
return (
|
||||
<MenuItem
|
||||
key={account.address.toBase58()}
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
setWalletSelector(account.selector);
|
||||
}}
|
||||
selected={account.isSelected}
|
||||
component="div"
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
{account.isSelected ? <CheckIcon fontSize="small" /> : null}
|
||||
</ListItemIcon>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography>{account.name}</Typography>
|
||||
<Typography color="textSecondary">
|
||||
{account.address.toBase58()}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography fontWeight="fontWeightBold">
|
||||
<b>Do not enter your hardware wallet seedphrase here.</b> Hardware
|
||||
wallets can be optionally connected after a web wallet is created.
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
|
@ -330,11 +334,9 @@ function RestoreWalletForm({ goBack }) {
|
|||
|
||||
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'),
|
||||
|
@ -356,52 +358,12 @@ function DerivedAccounts({ goBack, mnemonic, seed, password }) {
|
|||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<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>
|
||||
<AccountsSelector
|
||||
showDeprecated={true}
|
||||
accounts={accounts}
|
||||
dPathMenuItem={dPathMenuItem}
|
||||
setDPathMenuItem={setDPathMenuItem}
|
||||
/>
|
||||
<CardActions style={{ justifyContent: 'space-between' }}>
|
||||
<Button onClick={goBack}>Back</Button>
|
||||
<Button color="primary" onClick={submit}>
|
||||
|
@ -412,18 +374,80 @@ function DerivedAccounts({ goBack, mnemonic, seed, password }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function AccountsSelector({
|
||||
showRoot,
|
||||
showDeprecated,
|
||||
accounts,
|
||||
dPathMenuItem,
|
||||
setDPathMenuItem,
|
||||
onClick,
|
||||
}) {
|
||||
return (
|
||||
<CardContent>
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
{showRoot && (
|
||||
<MenuItem value={DerivationPathMenuItem.Bip44Root}>
|
||||
{`m/44'/501'`}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem value={DerivationPathMenuItem.Bip44}>
|
||||
{`m/44'/501'/0'`}
|
||||
</MenuItem>
|
||||
<MenuItem value={DerivationPathMenuItem.Bip44Change}>
|
||||
{`m/44'/501'/0'/0'`}
|
||||
</MenuItem>
|
||||
{showDeprecated && (
|
||||
<MenuItem value={DerivationPathMenuItem.Deprecated}>
|
||||
{`m/501'/0'/0/0 (deprecated)`}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
{accounts.map((acc) => {
|
||||
return (
|
||||
<div onClick={onClick ? () => onClick(acc) : {}}>
|
||||
<BalanceListItem
|
||||
key={acc.publicKey.toString()}
|
||||
onClick={onClick}
|
||||
publicKey={acc.publicKey}
|
||||
expandable={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
export const DerivationPathMenuItem = {
|
||||
Deprecated: 0,
|
||||
Bip44: 1,
|
||||
Bip44Change: 2,
|
||||
Bip44Root: 3, // Ledger only.
|
||||
};
|
||||
|
||||
function toDerivationPath(dPathMenuItem) {
|
||||
export function toDerivationPath(dPathMenuItem) {
|
||||
switch (dPathMenuItem) {
|
||||
case DerivationPathMenuItem.Deprecated:
|
||||
return DERIVATION_PATH.deprecated;
|
||||
|
@ -431,6 +455,8 @@ function toDerivationPath(dPathMenuItem) {
|
|||
return DERIVATION_PATH.bip44;
|
||||
case DerivationPathMenuItem.Bip44Change:
|
||||
return DERIVATION_PATH.bip44Change;
|
||||
case DerivationPathMenuItem.Bip44Root:
|
||||
return DERIVATION_PATH.bip44Root;
|
||||
default:
|
||||
throw new Error(`invalid derivation path: ${dPathMenuItem}`);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ export default function PopupPage({ opener }) {
|
|||
|
||||
const [connectedAccount, setConnectedAccount] = useState(null);
|
||||
const hasConnectedAccount = !!connectedAccount;
|
||||
|
||||
const [requests, setRequests] = useState(getInitialRequests);
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
const postMessage = useCallback(
|
||||
|
@ -66,6 +65,22 @@ export default function PopupPage({ opener }) {
|
|||
[opener, origin],
|
||||
);
|
||||
|
||||
// Hack to keep selectedWallet and wallet in sync. TODO: remove this block.
|
||||
useEffect(() => {
|
||||
if (!isExtension) {
|
||||
if (!wallet) {
|
||||
setWallet(selectedWallet);
|
||||
} else if (!wallet.publicKey.equals(selectedWallet.publicKey)) {
|
||||
setWallet(selectedWallet);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
wallet,
|
||||
wallet.publicKey,
|
||||
selectedWallet,
|
||||
selectedWallet.publicKey,
|
||||
]);
|
||||
|
||||
// (Extension only) Fetch connected wallet for site from local storage.
|
||||
useEffect(() => {
|
||||
if (isExtension) {
|
||||
|
@ -262,9 +277,19 @@ export default function PopupPage({ opener }) {
|
|||
}
|
||||
|
||||
async function sendAllSignatures(messages) {
|
||||
const signatures = await Promise.all(
|
||||
messages.map((m) => wallet.createSignature(m)),
|
||||
);
|
||||
console.log('wallet', wallet);
|
||||
let signatures;
|
||||
// Ledger must sign one by one.
|
||||
if (wallet.type === 'ledger') {
|
||||
signatures = [];
|
||||
for (let k = 0; k < messages.length; k += 1) {
|
||||
signatures.push(await wallet.createSignature(messages[k]));
|
||||
}
|
||||
} else {
|
||||
signatures = await Promise.all(
|
||||
messages.map((m) => wallet.createSignature(m)),
|
||||
);
|
||||
}
|
||||
postMessage({
|
||||
result: {
|
||||
signatures,
|
||||
|
@ -352,11 +377,11 @@ const useStyles = makeStyles((theme) => ({
|
|||
|
||||
function ApproveConnectionForm({ origin, onApprove }) {
|
||||
const wallet = useWallet();
|
||||
const { accounts } = useWalletSelector();
|
||||
const { accounts, hardwareWalletAccount } = useWalletSelector();
|
||||
// TODO better way to do this
|
||||
const account = accounts.find((account) =>
|
||||
account.address.equals(wallet.publicKey),
|
||||
);
|
||||
const account = accounts
|
||||
.concat([hardwareWalletAccount])
|
||||
.find((account) => account && account.address.equals(wallet.publicKey));
|
||||
const classes = useStyles();
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
let [dismissed, setDismissed] = useLocalStorageState(
|
||||
|
|
|
@ -7,7 +7,7 @@ interface Markets {
|
|||
publicKey: PublicKey;
|
||||
name: string;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const serumMarkets = (() => {
|
||||
|
@ -34,7 +34,7 @@ export const serumMarkets = (() => {
|
|||
|
||||
// Create a cached API wrapper to avoid rate limits.
|
||||
class PriceStore {
|
||||
cache: {}
|
||||
cache: {};
|
||||
|
||||
constructor() {
|
||||
this.cache = {};
|
||||
|
@ -50,7 +50,12 @@ class PriceStore {
|
|||
fetch(`https://serum-api.bonfida.com/orderbooks/${marketName}`).then(
|
||||
(resp) => {
|
||||
resp.json().then((resp) => {
|
||||
if (resp.data.asks.length === 0 && resp.data.bids.length === 0) {
|
||||
if (resp.data.asks === null || resp.data.bids === null) {
|
||||
resolve(undefined);
|
||||
} else if (
|
||||
resp.data.asks.length === 0 &&
|
||||
resp.data.bids.length === 0
|
||||
) {
|
||||
resolve(undefined);
|
||||
} else if (resp.data.asks.length === 0) {
|
||||
resolve(resp.data.bids[0].price);
|
||||
|
|
|
@ -172,17 +172,20 @@ export function WalletProvider({ children }) {
|
|||
{},
|
||||
);
|
||||
// `walletSelector` identifies which wallet to use.
|
||||
const [walletSelector, setWalletSelector] = useLocalStorageState(
|
||||
let [walletSelector, setWalletSelector] = useLocalStorageState(
|
||||
'walletSelector',
|
||||
DEFAULT_WALLET_SELECTOR,
|
||||
);
|
||||
const [ledgerPubKey, setLedgerPubKey] = useState(
|
||||
walletSelector.ledger ? walletSelector.importedPubkey : undefined,
|
||||
);
|
||||
const [_hardwareWalletAccount, setHardwareWalletAccount] = useState(null);
|
||||
|
||||
// `walletCount` is the number of HD wallets.
|
||||
const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1);
|
||||
|
||||
if (walletSelector.ledger && !_hardwareWalletAccount) {
|
||||
walletSelector = DEFAULT_WALLET_SELECTOR;
|
||||
setWalletSelector(DEFAULT_WALLET_SELECTOR);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!seed) {
|
||||
|
@ -193,9 +196,15 @@ export function WalletProvider({ children }) {
|
|||
try {
|
||||
const onDisconnect = () => {
|
||||
setWalletSelector(DEFAULT_WALLET_SELECTOR);
|
||||
setLedgerPubKey(undefined);
|
||||
setHardwareWalletAccount(null);
|
||||
};
|
||||
wallet = await Wallet.create(connection, 'ledger', { onDisconnect });
|
||||
const args = {
|
||||
onDisconnect,
|
||||
derivationPath: walletSelector.derivationPath,
|
||||
account: walletSelector.account,
|
||||
change: walletSelector.change,
|
||||
};
|
||||
wallet = await Wallet.create(connection, 'ledger', args);
|
||||
} catch (e) {
|
||||
console.log(`received error using ledger wallet: ${e}`);
|
||||
let message = 'Received error unlocking ledger';
|
||||
|
@ -204,7 +213,7 @@ export function WalletProvider({ children }) {
|
|||
}
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
setWalletSelector(DEFAULT_WALLET_SELECTOR);
|
||||
setLedgerPubKey(undefined);
|
||||
setHardwareWalletAccount(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -242,11 +251,8 @@ export function WalletProvider({ children }) {
|
|||
enqueueSnackbar,
|
||||
derivationPath,
|
||||
]);
|
||||
|
||||
function addAccount({ name, importedAccount, ledger }) {
|
||||
if (ledger) {
|
||||
setLedgerPubKey(importedAccount);
|
||||
} else if (importedAccount === undefined) {
|
||||
if (importedAccount === undefined) {
|
||||
name && localStorage.setItem(`name${walletCount}`, name);
|
||||
setWalletCount(walletCount + 1);
|
||||
} else {
|
||||
|
@ -319,29 +325,26 @@ export function WalletProvider({ children }) {
|
|||
};
|
||||
});
|
||||
|
||||
if (ledgerPubKey) {
|
||||
derivedAccounts.push({
|
||||
selector: {
|
||||
walletIndex: undefined,
|
||||
importedPubkey: ledgerPubKey,
|
||||
ledger: true,
|
||||
},
|
||||
address: new PublicKey(ledgerPubKey), // todo: get the ledger address
|
||||
name: 'Hardware wallet',
|
||||
isSelected: walletSelector.ledger,
|
||||
});
|
||||
}
|
||||
|
||||
return derivedAccounts.concat(importedAccounts);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
seed,
|
||||
walletCount,
|
||||
walletSelector,
|
||||
privateKeyImports,
|
||||
walletNames,
|
||||
ledgerPubKey,
|
||||
]);
|
||||
}, [seed, walletCount, walletSelector, privateKeyImports, walletNames]);
|
||||
|
||||
let hardwareWalletAccount;
|
||||
if (_hardwareWalletAccount) {
|
||||
hardwareWalletAccount = {
|
||||
..._hardwareWalletAccount,
|
||||
selector: {
|
||||
walletIndex: undefined,
|
||||
ledger: true,
|
||||
importedPubkey: _hardwareWalletAccount.publicKey,
|
||||
derivationPath: _hardwareWalletAccount.derivationPath,
|
||||
account: _hardwareWalletAccount.account,
|
||||
change: _hardwareWalletAccount.change,
|
||||
},
|
||||
address: _hardwareWalletAccount.publicKey,
|
||||
isSelected: walletSelector.ledger,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<WalletContext.Provider
|
||||
|
@ -358,6 +361,8 @@ export function WalletProvider({ children }) {
|
|||
addAccount,
|
||||
setAccountName,
|
||||
derivationPath,
|
||||
hardwareWalletAccount,
|
||||
setHardwareWalletAccount,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -473,7 +478,16 @@ export function useWalletSelector() {
|
|||
addAccount,
|
||||
setWalletSelector,
|
||||
setAccountName,
|
||||
hardwareWalletAccount,
|
||||
setHardwareWalletAccount,
|
||||
} = useContext(WalletContext);
|
||||
|
||||
return { accounts, setWalletSelector, addAccount, setAccountName };
|
||||
return {
|
||||
accounts,
|
||||
setWalletSelector,
|
||||
addAccount,
|
||||
setAccountName,
|
||||
hardwareWalletAccount,
|
||||
setHardwareWalletAccount,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import { DERIVATION_PATH } from './localStorage';
|
||||
const bs58 = require('bs58');
|
||||
|
||||
const INS_GET_PUBKEY = 0x05;
|
||||
|
@ -60,21 +61,43 @@ function _harden(n) {
|
|||
return (n | BIP32_HARDENED_BIT) >>> 0;
|
||||
}
|
||||
|
||||
export function solana_derivation_path(account, change) {
|
||||
const length = 4;
|
||||
export function solana_derivation_path(account, change, derivationPath) {
|
||||
let useAccount = account ? account : 0;
|
||||
let useChange = change ? change : 0;
|
||||
derivationPath = derivationPath
|
||||
? derivationPath
|
||||
: DERIVATION_PATH.bip44Change;
|
||||
|
||||
var derivation_path = Buffer.alloc(1 + length * 4);
|
||||
// eslint-disable-next-line
|
||||
var offset = 0;
|
||||
offset = derivation_path.writeUInt8(length, offset);
|
||||
offset = derivation_path.writeUInt32BE(_harden(44), offset); // Using BIP44
|
||||
offset = derivation_path.writeUInt32BE(_harden(501), offset); // Solana's BIP44 path
|
||||
offset = derivation_path.writeUInt32BE(_harden(useAccount), offset);
|
||||
derivation_path.writeUInt32BE(_harden(useChange), offset);
|
||||
|
||||
return derivation_path;
|
||||
if (derivationPath === DERIVATION_PATH.bip44Root) {
|
||||
const length = 2;
|
||||
const derivation_path = Buffer.alloc(1 + length * 4);
|
||||
let offset = 0;
|
||||
offset = derivation_path.writeUInt8(length, offset);
|
||||
offset = derivation_path.writeUInt32BE(_harden(44), offset); // Using BIP44
|
||||
derivation_path.writeUInt32BE(_harden(501), offset); // Solana's BIP44 path
|
||||
return derivation_path;
|
||||
} else if (derivationPath === DERIVATION_PATH.bip44) {
|
||||
const length = 3;
|
||||
const derivation_path = Buffer.alloc(1 + length * 4);
|
||||
let offset = 0;
|
||||
offset = derivation_path.writeUInt8(length, offset);
|
||||
offset = derivation_path.writeUInt32BE(_harden(44), offset); // Using BIP44
|
||||
offset = derivation_path.writeUInt32BE(_harden(501), offset); // Solana's BIP44 path
|
||||
derivation_path.writeUInt32BE(_harden(useAccount), offset);
|
||||
return derivation_path;
|
||||
} else if (derivationPath === DERIVATION_PATH.bip44Change) {
|
||||
const length = 4;
|
||||
const derivation_path = Buffer.alloc(1 + length * 4);
|
||||
let offset = 0;
|
||||
offset = derivation_path.writeUInt8(length, offset);
|
||||
offset = derivation_path.writeUInt32BE(_harden(44), offset); // Using BIP44
|
||||
offset = derivation_path.writeUInt32BE(_harden(501), offset); // Solana's BIP44 path
|
||||
offset = derivation_path.writeUInt32BE(_harden(useAccount), offset);
|
||||
derivation_path.writeUInt32BE(_harden(useChange), offset);
|
||||
return derivation_path;
|
||||
} else {
|
||||
throw new Error('Invalid derivation path');
|
||||
}
|
||||
}
|
||||
|
||||
async function solana_ledger_get_pubkey(transport, derivation_path) {
|
||||
|
@ -102,7 +125,6 @@ export async function solana_ledger_sign_bytes(
|
|||
) {
|
||||
var num_paths = Buffer.alloc(1);
|
||||
num_paths.writeUInt8(1);
|
||||
|
||||
const payload = Buffer.concat([num_paths, derivation_path, msg_bytes]);
|
||||
|
||||
return solana_send(transport, INS_SIGN_MESSAGE, P1_CONFIRM, payload);
|
||||
|
@ -123,3 +145,15 @@ export async function getPublicKey(transport, path) {
|
|||
|
||||
return new PublicKey(from_pubkey_string);
|
||||
}
|
||||
|
||||
export async function solana_ledger_confirm_public_key(
|
||||
transport,
|
||||
derivation_path,
|
||||
) {
|
||||
return await solana_send(
|
||||
transport,
|
||||
INS_GET_PUBKEY,
|
||||
P1_CONFIRM,
|
||||
derivation_path,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,17 +4,29 @@ import {
|
|||
solana_derivation_path,
|
||||
solana_ledger_sign_bytes,
|
||||
solana_ledger_sign_transaction,
|
||||
solana_ledger_confirm_public_key,
|
||||
} from './ledger-core';
|
||||
import { DERIVATION_PATH } from './localStorage';
|
||||
import bs58 from 'bs58';
|
||||
|
||||
export class LedgerWalletProvider {
|
||||
constructor(args) {
|
||||
this.onDisconnect = (args && args.onDisconnect) || (() => {});
|
||||
this.derivationPath = args
|
||||
? args.derivationPath
|
||||
: DERIVATION_PATH.bip44Change;
|
||||
this.account = args ? args.account : undefined;
|
||||
this.change = args ? args.change : undefined;
|
||||
this.solanaDerivationPath = solana_derivation_path(
|
||||
this.account,
|
||||
this.change,
|
||||
this.derivationPath,
|
||||
);
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
this.transport = await TransportWebUsb.create();
|
||||
this.pubKey = await getPublicKey(this.transport);
|
||||
this.pubKey = await getPublicKey(this.transport, this.solanaDerivationPath);
|
||||
this.transport.on('disconnect', this.onDisconnect);
|
||||
this.listAddresses = async (walletCount) => {
|
||||
// TODO: read accounts from ledger
|
||||
|
@ -28,10 +40,9 @@ export class LedgerWalletProvider {
|
|||
}
|
||||
|
||||
signTransaction = async (transaction) => {
|
||||
const from_derivation_path = solana_derivation_path();
|
||||
const sig_bytes = await solana_ledger_sign_transaction(
|
||||
this.transport,
|
||||
from_derivation_path,
|
||||
this.solanaDerivationPath,
|
||||
transaction,
|
||||
);
|
||||
transaction.addSignature(this.publicKey, sig_bytes);
|
||||
|
@ -39,12 +50,18 @@ export class LedgerWalletProvider {
|
|||
};
|
||||
|
||||
createSignature = async (message) => {
|
||||
const from_derivation_path = solana_derivation_path();
|
||||
const sig_bytes = await solana_ledger_sign_bytes(
|
||||
this.transport,
|
||||
from_derivation_path,
|
||||
this.solanaDerivationPath,
|
||||
message,
|
||||
);
|
||||
return bs58.encode(sig_bytes);
|
||||
};
|
||||
|
||||
confirmPublicKey = async () => {
|
||||
return await solana_ledger_confirm_public_key(
|
||||
this.transport,
|
||||
this.solanaDerivationPath,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export const DERIVATION_PATH = {
|
|||
deprecated: undefined,
|
||||
bip44: 'bip44',
|
||||
bip44Change: 'bip44Change',
|
||||
bip44Root: 'bip44Root', // Ledger only.
|
||||
};
|
||||
|
||||
export function getAccountFromSeed(
|
||||
|
|
|
@ -1862,10 +1862,10 @@
|
|||
schema-utils "^2.6.5"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@project-serum/serum@^0.13.24":
|
||||
version "0.13.24"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.24.tgz#42ab28052924c4b754ed078790a192ae23fffab9"
|
||||
integrity sha512-ux2MuO2kGGUV/QYBVx+e3roM/zATI6Fi2ThVaEo4aBdQKosbuNA60uAfSnYK9Du0kz7UjGoZHv2FnkCq8web3A==
|
||||
"@project-serum/serum@^0.13.33":
|
||||
version "0.13.33"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.33.tgz#03ce8219c1bb458f56c09dc8aa621d76538e70f5"
|
||||
integrity sha512-g2ztZwhQAvhGE9u4/Md6uEFBpaOMV2Xa/H/FGhgTx3iBv2sikW5fheHRJ8Vy7yEA9ZhZCuzbCkw8Wz1fq82VAg==
|
||||
dependencies:
|
||||
"@solana/web3.js" "^0.90.0"
|
||||
bn.js "^5.1.2"
|
||||
|
|
Loading…
Reference in New Issue