Merge branch 'master' into feature/bring_to_front
This commit is contained in:
commit
30adddc56a
|
@ -5,7 +5,7 @@
|
|||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@project-serum/serum": "^0.13.4",
|
||||
"@project-serum/serum": "^0.13.11",
|
||||
"@solana/web3.js": "^0.78.2",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useState } from 'react';
|
||||
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 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 (
|
||||
<DialogForm
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onSubmit={() => onAdd({ name, importedAccount })}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Add account</DialogTitle>
|
||||
<DialogContent style={{ paddingTop: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
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>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
<Button type="submit" color="primary" disabled={!isAddEnabled}>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogForm>
|
||||
);
|
||||
}
|
||||
|
||||
function decodeAccount(privateKey) {
|
||||
try {
|
||||
const a = new Account(bs58.decode(privateKey));
|
||||
return a;
|
||||
} catch (_) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -170,18 +170,20 @@ export default function AddTokenDialog({ open, onClose }) {
|
|||
</React.Fragment>
|
||||
) : tab === 'popular' ? (
|
||||
<List disablePadding>
|
||||
{popularTokens.map((token) => (
|
||||
<TokenListItem
|
||||
key={token.mintAddress}
|
||||
{...token}
|
||||
existingAccount={(walletAccounts || []).find(
|
||||
(account) =>
|
||||
account.parsed.mint.toBase58() === token.mintAddress,
|
||||
)}
|
||||
onSubmit={onSubmit}
|
||||
disalbed={sending}
|
||||
/>
|
||||
))}
|
||||
{popularTokens
|
||||
.filter((token) => !token.deprecated)
|
||||
.map((token) => (
|
||||
<TokenListItem
|
||||
key={token.mintAddress}
|
||||
{...token}
|
||||
existingAccount={(walletAccounts || []).find(
|
||||
(account) =>
|
||||
account.parsed.mint.toBase58() === token.mintAddress,
|
||||
)}
|
||||
onSubmit={onSubmit}
|
||||
disalbed={sending}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
) : tab === 'erc20' ? (
|
||||
<>
|
||||
|
|
|
@ -30,6 +30,7 @@ import IconButton from '@material-ui/core/IconButton';
|
|||
import InfoIcon from '@material-ui/icons/InfoOutlined';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import AddTokenDialog from './AddTokenDialog';
|
||||
import ExportAccountDialog from './ExportAccountDialog';
|
||||
import SendDialog from './SendDialog';
|
||||
import DepositDialog from './DepositDialog';
|
||||
import {
|
||||
|
@ -157,6 +158,7 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
|
|||
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||
const [depositDialogOpen, setDepositDialogOpen] = useState(false);
|
||||
const [tokenInfoDialogOpen, setTokenInfoDialogOpen] = useState(false);
|
||||
const [exportAccDialogOpen, setExportAccDialogOpen] = useState(false);
|
||||
const [
|
||||
closeTokenAccountDialogOpen,
|
||||
setCloseTokenAccountDialogOpen,
|
||||
|
@ -168,8 +170,16 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
|
|||
|
||||
let { mint, tokenName, tokenSymbol, owner, amount } = balanceInfo;
|
||||
|
||||
// Only show the export UI for the native SOL coin.
|
||||
const exportNeedsDisplay =
|
||||
mint === null && tokenName === 'SOL' && tokenSymbol === 'SOL';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExportAccountDialog
|
||||
onClose={() => setExportAccDialogOpen(false)}
|
||||
open={exportAccDialogOpen}
|
||||
/>
|
||||
<div className={classes.itemDetails}>
|
||||
<div className={classes.buttonContainer}>
|
||||
{!publicKey.equals(owner) && showTokenInfoDialog ? (
|
||||
|
@ -224,18 +234,31 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
|
|||
Token Address: {mint.toBase58()}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Solana Explorer
|
||||
</Link>
|
||||
</Typography>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
|
||||
urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
View on Solana Explorer
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
{exportNeedsDisplay && (
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link href={'#'} onClick={(e) => setExportAccDialogOpen(true)}>
|
||||
Export
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SendDialog
|
||||
open={sendDialogOpen}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import React, { useState } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
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 Switch from '@material-ui/core/Switch';
|
||||
import * as bs58 from 'bs58';
|
||||
import DialogForm from './DialogForm';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
|
||||
export default function ExportAccountDialog({ open, onClose }) {
|
||||
const wallet = useWallet();
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
|
||||
return (
|
||||
<DialogForm open={open} onClose={onClose} fullWidth>
|
||||
<DialogTitle>Export account</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Private key"
|
||||
fullWidth
|
||||
type={isHidden && 'password'}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={bs58.encode(wallet.account.secretKey)}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={!isHidden}
|
||||
onChange={() => setIsHidden(!isHidden)}
|
||||
/>
|
||||
}
|
||||
label="Reveal"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</DialogForm>
|
||||
);
|
||||
}
|
|
@ -19,6 +19,7 @@ import IconButton from '@material-ui/core/IconButton';
|
|||
import SolanaIcon from './SolanaIcon';
|
||||
import CodeIcon from '@material-ui/icons/Code';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import AddAccountDialog from './AddAccountDialog';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: {
|
||||
|
@ -125,16 +126,31 @@ function NetworkSelector() {
|
|||
}
|
||||
|
||||
function WalletSelector() {
|
||||
const { addresses, walletIndex, setWalletIndex } = useWalletSelector();
|
||||
const { accounts, setWalletSelector, addAccount } = useWalletSelector();
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [addAccountOpen, setAddAccountOpen] = useState(false);
|
||||
const classes = useStyles();
|
||||
|
||||
if (addresses.length === 0) {
|
||||
if (accounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddAccountDialog
|
||||
open={addAccountOpen}
|
||||
onClose={() => setAddAccountOpen(false)}
|
||||
onAdd={({ name, importedAccount }) => {
|
||||
addAccount({ name, importedAccount });
|
||||
setWalletSelector({
|
||||
walletIndex: importedAccount ? undefined : accounts.length,
|
||||
importedPubkey: importedAccount
|
||||
? importedAccount.publicKey.toString()
|
||||
: undefined,
|
||||
});
|
||||
setAddAccountOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Hidden xsDown>
|
||||
<Button
|
||||
color="inherit"
|
||||
|
@ -161,32 +177,38 @@ function WalletSelector() {
|
|||
}}
|
||||
getContentAnchorEl={null}
|
||||
>
|
||||
{addresses.map((address, index) => (
|
||||
{accounts.map(({ isSelected, selector, address, name, label }) => (
|
||||
<MenuItem
|
||||
key={address.toBase58()}
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
setWalletIndex(index);
|
||||
setWalletSelector(selector);
|
||||
}}
|
||||
selected={index === walletIndex}
|
||||
selected={isSelected}
|
||||
component="div"
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
{index === walletIndex ? <CheckIcon fontSize="small" /> : null}
|
||||
{isSelected ? <CheckIcon fontSize="small" /> : null}
|
||||
</ListItemIcon>
|
||||
{address.toBase58()}
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography>{name}</Typography>
|
||||
<Typography color="textSecondary">
|
||||
{address.toBase58()}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
setWalletIndex(addresses.length);
|
||||
setAddAccountOpen(true);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
<AddIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
Create Account
|
||||
Add Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
|
|
|
@ -5,7 +5,7 @@ import DialogTitle from '@material-ui/core/DialogTitle';
|
|||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import DialogForm from './DialogForm';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
import { useWallet, useWalletAddressForMint } from '../utils/wallet';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { abbreviateAddress } from '../utils/utils';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
|
@ -28,10 +28,20 @@ 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 { parseTokenAccountData } from '../utils/tokens/data';
|
||||
|
||||
const WUSDC_MINT = new PublicKey(
|
||||
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
|
||||
);
|
||||
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
|
||||
|
||||
export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
|
||||
const isProdNetwork = useIsProdNetwork();
|
||||
const [tab, setTab] = useState(0);
|
||||
const [tab, setTab] = useState('spl');
|
||||
const onSubmitRef = useRef();
|
||||
|
||||
const [swapCoinInfo] = useSwapApiGet(
|
||||
|
@ -49,6 +59,7 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
|
|||
open={open}
|
||||
onClose={onClose}
|
||||
onSubmit={() => onSubmitRef.current()}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Send {tokenName ?? abbreviateAddress(mint)}
|
||||
|
@ -62,23 +73,52 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
|
|||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
>
|
||||
<Tab label={`SPL ${swapCoinInfo.ticker}`} />
|
||||
<Tab
|
||||
label={`${swapCoinInfo.erc20Contract ? 'ERC20' : 'Native'} ${
|
||||
swapCoinInfo.ticker
|
||||
}`}
|
||||
/>
|
||||
{mint?.equals(WUSDC_MINT)
|
||||
? [
|
||||
<Tab label="SPL WUSDC" key="spl" value="spl" />,
|
||||
<Tab
|
||||
label="SPL USDC"
|
||||
key="wusdcToSplUsdc"
|
||||
value="wusdcToSplUsdc"
|
||||
/>,
|
||||
<Tab label="ERC20 USDC" key="swap" value="swap" />,
|
||||
]
|
||||
: [
|
||||
<Tab
|
||||
label={`SPL ${swapCoinInfo.ticker}`}
|
||||
key="spl"
|
||||
value="spl"
|
||||
/>,
|
||||
<Tab
|
||||
label={`${
|
||||
swapCoinInfo.erc20Contract ? 'ERC20' : 'Native'
|
||||
} ${swapCoinInfo.ticker}`}
|
||||
key="swap"
|
||||
value="swap"
|
||||
/>,
|
||||
]}
|
||||
</Tabs>
|
||||
) : null}
|
||||
{tab === 0 ? (
|
||||
{tab === 'spl' ? (
|
||||
<SendSplDialog
|
||||
onClose={onClose}
|
||||
publicKey={publicKey}
|
||||
balanceInfo={balanceInfo}
|
||||
onSubmitRef={onSubmitRef}
|
||||
/>
|
||||
) : tab === 'wusdcToSplUsdc' ? (
|
||||
<SendSwapDialog
|
||||
key={tab}
|
||||
onClose={onClose}
|
||||
publicKey={publicKey}
|
||||
balanceInfo={balanceInfo}
|
||||
swapCoinInfo={swapCoinInfo}
|
||||
onSubmitRef={onSubmitRef}
|
||||
wusdcToSplUsdc
|
||||
/>
|
||||
) : (
|
||||
<SendSwapDialog
|
||||
key={tab}
|
||||
onClose={onClose}
|
||||
publicKey={publicKey}
|
||||
balanceInfo={balanceInfo}
|
||||
|
@ -97,15 +137,60 @@ 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 wallet = useWallet();
|
||||
const [sendTransaction, sending] = useSendTransaction();
|
||||
const [addressHelperText, setAddressHelperText] = useState(
|
||||
defaultAddressHelperText,
|
||||
);
|
||||
const [passValidation, setPassValidation] = useState();
|
||||
const {
|
||||
fields,
|
||||
destinationAddress,
|
||||
transferAmountString,
|
||||
validAmount,
|
||||
} = useForm(balanceInfo);
|
||||
const { decimals } = balanceInfo;
|
||||
} = useForm(balanceInfo, addressHelperText, passValidation);
|
||||
const { decimals, mint } = balanceInfo;
|
||||
const mintString = mint && mint.toBase58();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!destinationAddress) {
|
||||
setAddressHelperText(defaultAddressHelperText);
|
||||
setPassValidation(undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const destinationAccountInfo = await wallet.connection.getAccountInfo(
|
||||
new PublicKey(destinationAddress),
|
||||
);
|
||||
|
||||
if (destinationAccountInfo.owner.equals(TOKEN_PROGRAM_ID)) {
|
||||
const accountInfo = parseTokenAccountData(
|
||||
destinationAccountInfo.data,
|
||||
);
|
||||
if (accountInfo.mint.toBase58() === mintString) {
|
||||
setPassValidation(true);
|
||||
setAddressHelperText('Address is a valid SPL token address');
|
||||
} else {
|
||||
setPassValidation(false);
|
||||
setAddressHelperText('Destination address mint does not match');
|
||||
}
|
||||
} else {
|
||||
setPassValidation(true);
|
||||
setAddressHelperText('Destination is a Solana address');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Received error validating address ${e}`);
|
||||
setAddressHelperText(defaultAddressHelperText);
|
||||
setPassValidation(undefined);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [destinationAddress, wallet, mintString]);
|
||||
|
||||
async function makeTransaction() {
|
||||
let amount = Math.round(parseFloat(transferAmountString) * 10 ** decimals);
|
||||
|
@ -116,6 +201,7 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
|
|||
publicKey,
|
||||
new PublicKey(destinationAddress),
|
||||
amount,
|
||||
balanceInfo.mint,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -146,6 +232,7 @@ function SendSwapDialog({
|
|||
balanceInfo,
|
||||
swapCoinInfo,
|
||||
ethAccount,
|
||||
wusdcToSplUsdc = false,
|
||||
onSubmitRef,
|
||||
}) {
|
||||
const wallet = useWallet();
|
||||
|
@ -159,9 +246,12 @@ function SendSwapDialog({
|
|||
validAmount,
|
||||
} = useForm(balanceInfo);
|
||||
|
||||
const { tokenName, decimals } = balanceInfo;
|
||||
const blockchain =
|
||||
swapCoinInfo.blockchain === 'sol' ? 'eth' : swapCoinInfo.blockchain;
|
||||
const { tokenName, decimals, mint } = balanceInfo;
|
||||
const blockchain = wusdcToSplUsdc
|
||||
? 'sol'
|
||||
: swapCoinInfo.blockchain === 'sol'
|
||||
? 'eth'
|
||||
: swapCoinInfo.blockchain;
|
||||
const needMetamask = blockchain === 'eth';
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -170,17 +260,34 @@ function SendSwapDialog({
|
|||
}
|
||||
}, [blockchain, ethAccount, setDestinationAddress]);
|
||||
|
||||
let splUsdcWalletAddress = useWalletAddressForMint(
|
||||
wusdcToSplUsdc ? USDC_MINT : null,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (wusdcToSplUsdc && splUsdcWalletAddress) {
|
||||
setDestinationAddress(splUsdcWalletAddress);
|
||||
}
|
||||
}, [setDestinationAddress, wusdcToSplUsdc, splUsdcWalletAddress]);
|
||||
|
||||
async function makeTransaction() {
|
||||
let amount = Math.round(parseFloat(transferAmountString) * 10 ** decimals);
|
||||
if (!amount || amount <= 0) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
const swapInfo = await swapApiRequest('POST', 'swap_to', {
|
||||
const params = {
|
||||
blockchain,
|
||||
coin: swapCoinInfo.erc20Contract,
|
||||
address: destinationAddress,
|
||||
size: amount / 10 ** decimals,
|
||||
});
|
||||
};
|
||||
if (blockchain === 'sol') {
|
||||
params.coin = swapCoinInfo.splMint;
|
||||
} else if (blockchain === 'eth') {
|
||||
params.coin = swapCoinInfo.erc20Contract;
|
||||
}
|
||||
if (mint?.equals(WUSDC_MINT)) {
|
||||
params.wusdcToUsdc = true;
|
||||
}
|
||||
const swapInfo = await swapApiRequest('POST', 'swap_to', params);
|
||||
if (swapInfo.blockchain !== 'sol') {
|
||||
throw new Error('Unexpected blockchain');
|
||||
}
|
||||
|
@ -188,6 +295,7 @@ function SendSwapDialog({
|
|||
publicKey,
|
||||
new PublicKey(swapInfo.address),
|
||||
amount,
|
||||
balanceInfo.mint,
|
||||
swapInfo.memo,
|
||||
);
|
||||
}
|
||||
|
@ -203,6 +311,7 @@ function SendSwapDialog({
|
|||
key={signature}
|
||||
publicKey={publicKey}
|
||||
signature={signature}
|
||||
blockchain={blockchain}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
|
@ -213,7 +322,11 @@ function SendSwapDialog({
|
|||
<DialogContent style={{ paddingTop: 16 }}>
|
||||
<DialogContentText>
|
||||
SPL {tokenName} can be converted to{' '}
|
||||
{swapCoinInfo.erc20Contract ? 'ERC20' : 'native'}{' '}
|
||||
{blockchain === 'eth' && swapCoinInfo.erc20Contract
|
||||
? 'ERC20'
|
||||
: blockchain === 'sol' && swapCoinInfo.splMint
|
||||
? 'SPL'
|
||||
: 'native'}{' '}
|
||||
{swapCoinInfo.ticker}
|
||||
{needMetamask ? ' via MetaMask' : null}.
|
||||
</DialogContentText>
|
||||
|
@ -233,7 +346,7 @@ function SendSwapDialog({
|
|||
);
|
||||
}
|
||||
|
||||
function SendSwapProgress({ publicKey, signature, onClose }) {
|
||||
function SendSwapProgress({ publicKey, signature, onClose, blockchain }) {
|
||||
const connection = useConnection();
|
||||
const [swaps] = useSwapApiGet(`swaps_from/sol/${publicKey.toBase58()}`, {
|
||||
refreshInterval: 1000,
|
||||
|
@ -255,6 +368,8 @@ function SendSwapProgress({ publicKey, signature, onClose }) {
|
|||
if (withdrawal.txid?.startsWith('0x')) {
|
||||
step = 3;
|
||||
ethTxid = withdrawal.txid;
|
||||
} else if (withdrawal.txid && blockchain !== 'eth') {
|
||||
step = 3;
|
||||
} else {
|
||||
step = 2;
|
||||
}
|
||||
|
@ -285,7 +400,7 @@ function SendSwapProgress({ publicKey, signature, onClose }) {
|
|||
View on Etherscan
|
||||
</Link>
|
||||
</Typography>
|
||||
) : (
|
||||
) : step < 3 ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
|
@ -302,7 +417,13 @@ function SendSwapProgress({ publicKey, signature, onClose }) {
|
|||
<Typography>Transaction Pending</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
{!ethTxid && blockchain === 'eth' ? (
|
||||
<DialogContentText style={{ marginTop: 16, marginBottom: 0 }}>
|
||||
Please keep this window open. You will need to approve the request
|
||||
on MetaMask to complete the transaction.
|
||||
</DialogContentText>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
|
@ -311,7 +432,7 @@ function SendSwapProgress({ publicKey, signature, onClose }) {
|
|||
);
|
||||
}
|
||||
|
||||
function useForm(balanceInfo) {
|
||||
function useForm(balanceInfo, addressHelperText, passAddressValidation) {
|
||||
const [destinationAddress, setDestinationAddress] = useState('');
|
||||
const [transferAmountString, setTransferAmountString] = useState('');
|
||||
const { amount: balanceAmount, decimals, tokenSymbol } = balanceInfo;
|
||||
|
@ -328,6 +449,13 @@ function useForm(balanceInfo) {
|
|||
margin="normal"
|
||||
value={destinationAddress}
|
||||
onChange={(e) => setDestinationAddress(e.target.value.trim())}
|
||||
helperText={addressHelperText}
|
||||
id={
|
||||
!passAddressValidation && passAddressValidation !== undefined
|
||||
? 'outlined-error-helper-text'
|
||||
: undefined
|
||||
}
|
||||
error={!passAddressValidation && passAddressValidation !== undefined}
|
||||
/>
|
||||
<TextField
|
||||
label="Amount"
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
||||
import {
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
Account,
|
||||
} from '@solana/web3.js';
|
||||
import {
|
||||
assertOwner,
|
||||
closeAccount,
|
||||
initializeAccount,
|
||||
initializeMint,
|
||||
|
@ -8,7 +14,12 @@ import {
|
|||
TOKEN_PROGRAM_ID,
|
||||
transfer,
|
||||
} from './instructions';
|
||||
import { ACCOUNT_LAYOUT, getOwnedAccountsFilters, MINT_LAYOUT } from './data';
|
||||
import {
|
||||
ACCOUNT_LAYOUT,
|
||||
getOwnedAccountsFilters,
|
||||
MINT_LAYOUT,
|
||||
parseTokenAccountData,
|
||||
} from './data';
|
||||
import bs58 from 'bs58';
|
||||
|
||||
export async function getOwnedTokenAccounts(connection, publicKey) {
|
||||
|
@ -157,6 +168,61 @@ export async function transferTokens({
|
|||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
mint,
|
||||
}) {
|
||||
const destinationAccountInfo = await connection.getAccountInfo(
|
||||
destinationPublicKey,
|
||||
);
|
||||
if (!!destinationAccountInfo && destinationAccountInfo.owner.equals(TOKEN_PROGRAM_ID)) {
|
||||
return await transferBetweenSplTokenAccounts({
|
||||
connection,
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
});
|
||||
}
|
||||
if (!destinationAccountInfo || destinationAccountInfo.lamports === 0) {
|
||||
throw new Error('Cannot send to address with zero SOL balances');
|
||||
}
|
||||
const destinationSplTokenAccount = (
|
||||
await getOwnedTokenAccounts(connection, destinationPublicKey)
|
||||
)
|
||||
.map(({ publicKey, accountInfo }) => {
|
||||
return { publicKey, parsed: parseTokenAccountData(accountInfo.data) };
|
||||
})
|
||||
.filter(({ parsed }) => parsed.mint.equals(mint))
|
||||
.sort((a, b) => {
|
||||
return b.parsed.amount - a.parsed.amount;
|
||||
})[0];
|
||||
if (destinationSplTokenAccount) {
|
||||
return await transferBetweenSplTokenAccounts({
|
||||
connection,
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey: destinationSplTokenAccount.publicKey,
|
||||
amount,
|
||||
memo,
|
||||
});
|
||||
}
|
||||
return await createAndTransferToAccount({
|
||||
connection,
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
mint,
|
||||
});
|
||||
}
|
||||
|
||||
function createTransferBetweenSplTokenAccountsInstruction({
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
}) {
|
||||
let transaction = new Transaction().add(
|
||||
transfer({
|
||||
|
@ -169,12 +235,81 @@ export async function transferTokens({
|
|||
if (memo) {
|
||||
transaction.add(memoInstruction(memo));
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async function transferBetweenSplTokenAccounts({
|
||||
connection,
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
}) {
|
||||
const transaction = createTransferBetweenSplTokenAccountsInstruction({
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
});
|
||||
let signers = [owner];
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
}
|
||||
|
||||
async function createAndTransferToAccount({
|
||||
connection,
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
mint,
|
||||
}) {
|
||||
const newAccount = new Account();
|
||||
let transaction = new Transaction();
|
||||
transaction.add(
|
||||
assertOwner({
|
||||
account: destinationPublicKey,
|
||||
owner: SystemProgram.programId,
|
||||
}),
|
||||
);
|
||||
transaction.add(
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: owner.publicKey,
|
||||
newAccountPubkey: newAccount.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(
|
||||
ACCOUNT_LAYOUT.span,
|
||||
),
|
||||
space: ACCOUNT_LAYOUT.span,
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
}),
|
||||
);
|
||||
transaction.add(
|
||||
initializeAccount({
|
||||
account: newAccount.publicKey,
|
||||
mint,
|
||||
owner: destinationPublicKey,
|
||||
}),
|
||||
);
|
||||
const transferBetweenAccountsTxn = createTransferBetweenSplTokenAccountsInstruction(
|
||||
{
|
||||
owner,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey: newAccount.publicKey,
|
||||
amount,
|
||||
memo,
|
||||
},
|
||||
);
|
||||
transaction.add(transferBetweenAccountsTxn);
|
||||
let signers = [owner, newAccount];
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
}
|
||||
|
||||
export async function closeTokenAccount({
|
||||
connection,
|
||||
owner,
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
SYSVAR_RENT_PUBKEY,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import { publicKeyLayout } from '@project-serum/serum/lib/layout';
|
||||
|
||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
|
||||
|
@ -150,3 +151,26 @@ export function memoInstruction(memo) {
|
|||
programId: MEMO_PROGRAM_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export const OWNER_VALIDATION_PROGRAM_ID = new PublicKey(
|
||||
'4MNPdKu9wFMvEeZBMt3Eipfs5ovVWTJb31pEXDJAAxX5',
|
||||
);
|
||||
|
||||
export const OWNER_VALIDATION_LAYOUT = BufferLayout.struct([
|
||||
publicKeyLayout('account'),
|
||||
]);
|
||||
|
||||
export function encodeOwnerValidationInstruction(instruction) {
|
||||
const b = Buffer.alloc(OWNER_VALIDATION_LAYOUT.span);
|
||||
const span = OWNER_VALIDATION_LAYOUT.encode(instruction, b);
|
||||
return b.slice(0, span);
|
||||
}
|
||||
|
||||
export function assertOwner({ account, owner }) {
|
||||
const keys = [{ pubkey: account, isSigner: false, isWritable: false }];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
data: encodeOwnerValidationInstruction({ account: owner }),
|
||||
programId: OWNER_VALIDATION_PROGRAM_ID,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -70,10 +70,18 @@ export const TOKENS = {
|
|||
},
|
||||
{
|
||||
tokenSymbol: 'USDC',
|
||||
mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
tokenName: 'USD Coin',
|
||||
icon:
|
||||
'https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
|
||||
},
|
||||
{
|
||||
tokenSymbol: 'WUSDC',
|
||||
mintAddress: 'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
|
||||
tokenName: 'Wrapped USDC',
|
||||
icon:
|
||||
'https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
tokenSymbol: 'SUSHI',
|
||||
|
@ -145,9 +153,21 @@ export const TOKENS = {
|
|||
'https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png',
|
||||
},
|
||||
{
|
||||
tokenSymbol: 'ALEPH',
|
||||
mintAddress: 'CsZ5LZkDS7h9TDKjrbL7VAwQZ9nsRu8vJLhRYfmGaN8K',
|
||||
tokenName: 'Wrapped ALEPH',
|
||||
tokenSymbol: 'MATH',
|
||||
mintAddress: 'GeDS162t9yGJuLEHPWXXGrb1zwkzinCgRwnT8vHYjKza',
|
||||
tokenName: 'Wrapped MATH',
|
||||
},
|
||||
{
|
||||
tokenSymbol: 'TOMO',
|
||||
mintAddress: 'GXMvfY2jpQctDqZ9RoU3oWPhufKiCcFEfchvYumtX7jd',
|
||||
tokenName: 'Wrapped TOMO',
|
||||
icon: "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/tomochain/info/logo.png"
|
||||
},
|
||||
{
|
||||
tokenSymbol: 'LUA',
|
||||
mintAddress: 'EqWCKXfs3x47uVosDpTRgFniThL9Y8iCztJaapxbEaVX',
|
||||
tokenName: 'Wrapped LUA',
|
||||
icon: 'https://raw.githubusercontent.com/trustwallet/assets/2d2491130e6beda208ba4fc6df028a82a0106ab6/blockchains/ethereum/assets/0xB1f66997A5760428D3a87D68b90BfE0aE64121cC/logo.png',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -168,13 +188,11 @@ export function useTokenName(mint) {
|
|||
}
|
||||
|
||||
let info = customTokenNamesByNetwork?.[endpoint]?.[mint.toBase58()];
|
||||
if (!info) {
|
||||
let match = TOKENS?.[endpoint]?.find(
|
||||
(token) => token.mintAddress === mint.toBase58(),
|
||||
);
|
||||
if (match) {
|
||||
info = { name: match.tokenName, symbol: match.tokenSymbol };
|
||||
}
|
||||
let match = TOKENS?.[endpoint]?.find(
|
||||
(token) => token.mintAddress === mint.toBase58(),
|
||||
);
|
||||
if (match && (!info || match.deprecated)) {
|
||||
info = { name: match.tokenName, symbol: match.tokenSymbol };
|
||||
}
|
||||
return { name: info?.name, symbol: info?.symbol };
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { pbkdf2 } from 'crypto';
|
||||
import { randomBytes, secretbox } from 'tweetnacl';
|
||||
import * as bip32 from 'bip32';
|
||||
import bs58 from 'bs58';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
|
@ -19,11 +20,20 @@ export async function mnemonicToSeed(mnemonic) {
|
|||
return Buffer.from(seed).toString('hex');
|
||||
}
|
||||
|
||||
let unlockedMnemonicAndSeed = JSON.parse(
|
||||
sessionStorage.getItem('unlocked') ||
|
||||
localStorage.getItem('unlocked') ||
|
||||
'null',
|
||||
) || { mnemonic: null, seed: null };
|
||||
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,
|
||||
};
|
||||
})();
|
||||
export const walletSeedChanged = new EventEmitter();
|
||||
|
||||
export function getUnlockedMnemonicAndSeed() {
|
||||
|
@ -34,8 +44,8 @@ export function hasLockedMnemonicAndSeed() {
|
|||
return !!localStorage.getItem('locked');
|
||||
}
|
||||
|
||||
function setUnlockedMnemonicAndSeed(mnemonic, seed) {
|
||||
unlockedMnemonicAndSeed = { mnemonic, seed };
|
||||
function setUnlockedMnemonicAndSeed(mnemonic, seed, importsEncryptionKey) {
|
||||
unlockedMnemonicAndSeed = { mnemonic, seed, importsEncryptionKey };
|
||||
walletSeedChanged.emit('change', unlockedMnemonicAndSeed);
|
||||
}
|
||||
|
||||
|
@ -67,7 +77,8 @@ export async function storeMnemonicAndSeed(mnemonic, seed, password) {
|
|||
localStorage.removeItem('locked');
|
||||
sessionStorage.removeItem('unlocked');
|
||||
}
|
||||
setUnlockedMnemonicAndSeed(mnemonic, seed);
|
||||
const privateKey = deriveImportsEncryptionKey(seed);
|
||||
setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey);
|
||||
}
|
||||
|
||||
export async function loadMnemonicAndSeed(password, stayLoggedIn) {
|
||||
|
@ -91,7 +102,8 @@ export async function loadMnemonicAndSeed(password, stayLoggedIn) {
|
|||
if (stayLoggedIn) {
|
||||
sessionStorage.setItem('unlocked', decodedPlaintext);
|
||||
}
|
||||
setUnlockedMnemonicAndSeed(mnemonic, seed);
|
||||
const privateKey = deriveImportsEncryptionKey(seed);
|
||||
setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey);
|
||||
return { mnemonic, seed };
|
||||
}
|
||||
|
||||
|
@ -109,5 +121,12 @@ async function deriveEncryptionKey(password, salt, iterations, digest) {
|
|||
}
|
||||
|
||||
export function lockWallet() {
|
||||
setUnlockedMnemonicAndSeed(null, null);
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import React, { useContext, useMemo } from 'react';
|
||||
import * as bip32 from 'bip32';
|
||||
import { Account, SystemProgram, Transaction } from '@solana/web3.js';
|
||||
import * as bs58 from 'bs58';
|
||||
import {
|
||||
Account,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import nacl from 'tweetnacl';
|
||||
import {
|
||||
setInitialAccountInfo,
|
||||
|
@ -24,12 +30,15 @@ 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, seed, walletIndex = 0) {
|
||||
constructor(connection, account) {
|
||||
this.connection = connection;
|
||||
this.seed = seed;
|
||||
this.walletIndex = walletIndex;
|
||||
this.account = Wallet.getAccountFromSeed(this.seed, this.walletIndex);
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
|
||||
|
@ -69,7 +78,7 @@ export class Wallet {
|
|||
);
|
||||
};
|
||||
|
||||
transferToken = async (source, destination, amount, memo = null) => {
|
||||
transferToken = async (source, destination, amount, mint, memo = null) => {
|
||||
if (source.equals(this.publicKey)) {
|
||||
if (memo) {
|
||||
throw new Error('Memo not implemented');
|
||||
|
@ -83,6 +92,7 @@ export class Wallet {
|
|||
destinationPublicKey: destination,
|
||||
amount,
|
||||
memo,
|
||||
mint,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -112,19 +122,63 @@ const WalletContext = React.createContext(null);
|
|||
|
||||
export function WalletProvider({ children }) {
|
||||
useListener(walletSeedChanged, 'change');
|
||||
const { mnemonic, seed } = getUnlockedMnemonicAndSeed();
|
||||
const { mnemonic, seed, importsEncryptionKey } = getUnlockedMnemonicAndSeed();
|
||||
const connection = useConnection();
|
||||
const [walletIndex, setWalletIndex] = useLocalStorageState('walletIndex', 0);
|
||||
const wallet = useMemo(
|
||||
() =>
|
||||
seed
|
||||
? new Wallet(connection, Buffer.from(seed, 'hex'), walletIndex)
|
||||
: null,
|
||||
[connection, seed, walletIndex],
|
||||
|
||||
// `privateKeyImports` are accounts imported *in addition* to HD wallets
|
||||
const [privateKeyImports, setPrivateKeyImports] = useLocalStorageState(
|
||||
'walletPrivateKeyImports',
|
||||
{},
|
||||
);
|
||||
// `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 (
|
||||
<WalletContext.Provider
|
||||
value={{ wallet, walletIndex, setWalletIndex, seed, mnemonic }}
|
||||
value={{
|
||||
wallet,
|
||||
seed,
|
||||
mnemonic,
|
||||
importsEncryptionKey,
|
||||
walletSelector,
|
||||
setWalletSelector,
|
||||
privateKeyImports,
|
||||
setPrivateKeyImports,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WalletContext.Provider>
|
||||
|
@ -165,6 +219,19 @@ export function refreshWalletPublicKeys(wallet) {
|
|||
refreshCache(wallet.getTokenAccountInfo);
|
||||
}
|
||||
|
||||
export function useWalletAddressForMint(mint) {
|
||||
const [walletAccounts] = useWalletTokenAccounts();
|
||||
return useMemo(
|
||||
() =>
|
||||
mint
|
||||
? walletAccounts
|
||||
?.find((account) => account.parsed?.mint?.equals(mint))
|
||||
?.publicKey.toBase58()
|
||||
: null,
|
||||
[walletAccounts, mint],
|
||||
);
|
||||
}
|
||||
|
||||
export function useBalanceInfo(publicKey) {
|
||||
let [accountInfo, accountInfoLoaded] = useAccountInfo(publicKey);
|
||||
let { mint, owner, amount } = accountInfo?.owner.equals(TOKEN_PROGRAM_ID)
|
||||
|
@ -230,31 +297,66 @@ export function useBalanceInfo(publicKey) {
|
|||
}
|
||||
|
||||
export function useWalletSelector() {
|
||||
const { walletIndex, setWalletIndex, seed } = useContext(WalletContext);
|
||||
const {
|
||||
seed,
|
||||
importsEncryptionKey,
|
||||
walletSelector,
|
||||
setWalletSelector,
|
||||
privateKeyImports,
|
||||
setPrivateKeyImports,
|
||||
} = useContext(WalletContext);
|
||||
|
||||
// `walletCount` is the number of HD wallets.
|
||||
const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1);
|
||||
function selectWallet(walletIndex) {
|
||||
if (walletIndex >= walletCount) {
|
||||
setWalletCount(walletIndex + 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);
|
||||
}
|
||||
setWalletIndex(walletIndex);
|
||||
}
|
||||
const addresses = useMemo(() => {
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
if (!seed) {
|
||||
return [];
|
||||
}
|
||||
const seedBuffer = Buffer.from(seed, 'hex');
|
||||
return [...Array(walletCount).keys()].map(
|
||||
(walletIndex) =>
|
||||
Wallet.getAccountFromSeed(seedBuffer, walletIndex).publicKey,
|
||||
);
|
||||
}, [seed, walletCount]);
|
||||
return { addresses, 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;
|
||||
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}`,
|
||||
};
|
||||
});
|
||||
|
||||
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 };
|
||||
}
|
||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -1578,12 +1578,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
||||
|
||||
"@project-serum/serum@^0.13.4":
|
||||
version "0.13.4"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.4.tgz#5a7a2d6e418fa986a92c3e9dd3c04776ce3b5f5d"
|
||||
integrity sha512-fUruHI17rUrrl5DhuLIChGO1fhcJWwkc/GROToMwfsrHGUDA1FX0MOiXfDYQJaXvMJKJg/cMa6aNThShr05tmQ==
|
||||
"@project-serum/serum@^0.13.11":
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.11.tgz#c55c830cbfb28cdc2b4e4076b1367d45ad19175d"
|
||||
integrity sha512-kE9PhXuryP7afJFkZrKkLzNxeRETHgPvN5Jl3BZIDH5oyHLUfmj3OQreMPwIqJXnMkLwCHewG5JFJALwit+3Wg==
|
||||
dependencies:
|
||||
"@solana/web3.js" "^0.71.10"
|
||||
"@solana/web3.js" "0.86.1"
|
||||
bn.js "^5.1.2"
|
||||
buffer-layout "^1.2.0"
|
||||
|
||||
|
@ -1597,10 +1597,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
||||
|
||||
"@solana/web3.js@^0.71.10":
|
||||
version "0.71.14"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.71.14.tgz#b21f9613cb2e27defc93264bd894761689d209e3"
|
||||
integrity sha512-23jWjzMxSOKzcAUzLBaD5p0YRJys6A9cEdWZQtPV/CV7bmo5JNIdPDR+UhzPxe1L3WX3bu7KAmAjcIERCXKDfQ==
|
||||
"@solana/web3.js@0.86.1":
|
||||
version "0.86.1"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.86.1.tgz#034a2cef742569f74dfc9960dfbcabc92e674b08"
|
||||
integrity sha512-9mjWs17ym7PIm7bHA37wnnYyD7rIVHwkx1RI6BzGhMO5h8E+HlZM8ISLgOx+NItg8XRCfFhlrVgJTzK4om1s0g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
bn.js "^5.0.0"
|
||||
|
@ -1610,10 +1610,12 @@
|
|||
crypto-hash "^1.2.2"
|
||||
esdoc-inject-style-plugin "^1.0.0"
|
||||
jayson "^3.0.1"
|
||||
keccak "^3.0.1"
|
||||
mz "^2.7.0"
|
||||
node-fetch "^2.2.0"
|
||||
npm-run-all "^4.1.5"
|
||||
rpc-websockets "^7.4.0"
|
||||
rpc-websockets "^7.4.2"
|
||||
secp256k1 "^4.0.2"
|
||||
superstruct "^0.8.3"
|
||||
tweetnacl "^1.0.0"
|
||||
ws "^7.0.0"
|
||||
|
@ -7599,7 +7601,7 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
|
|||
array-includes "^3.1.1"
|
||||
object.assign "^4.1.0"
|
||||
|
||||
keccak@^3.0.0:
|
||||
keccak@^3.0.0, keccak@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff"
|
||||
integrity sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==
|
||||
|
@ -10725,21 +10727,6 @@ rlp@^2.2.3:
|
|||
dependencies:
|
||||
bn.js "^4.11.1"
|
||||
|
||||
rpc-websockets@^7.4.0:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.4.2.tgz#9e85ca7451e64a2015996c7a361cbff37c3ad597"
|
||||
integrity sha512-kUpYcnbEU/BeAxGTlfySZ/tp9FU+TLSgONbViyx6hQsIh8876uxggJWzVOCe+CztBvuCOAOd0BXyPlKfcflykw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
assert-args "^1.2.1"
|
||||
circular-json "^0.5.9"
|
||||
eventemitter3 "^4.0.7"
|
||||
uuid "^8.3.0"
|
||||
ws "^7.3.1"
|
||||
optionalDependencies:
|
||||
bufferutil "^4.0.1"
|
||||
utf-8-validate "^5.0.2"
|
||||
|
||||
rpc-websockets@^7.4.2:
|
||||
version "7.4.6"
|
||||
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.4.6.tgz#a0053ad36e893774cdd0edb72ac577deaf34f247"
|
||||
|
@ -10875,7 +10862,7 @@ scrypt-js@^3.0.0, scrypt-js@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312"
|
||||
integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==
|
||||
|
||||
secp256k1@^4.0.1:
|
||||
secp256k1@^4.0.1, secp256k1@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1"
|
||||
integrity sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==
|
||||
|
|
Loading…
Reference in New Issue