Merge branch 'master' into armani/wormhole

This commit is contained in:
Nathaniel Parke 2021-04-29 12:22:35 +08:00
commit a0a3e377e1
17 changed files with 334 additions and 148 deletions

View File

@ -2,7 +2,7 @@
Example Solana wallet with support for [SPL tokens](https://spl.solana.com/token) and Serum integration. Example Solana wallet with support for [SPL tokens](https://spl.solana.com/token) and Serum integration.
See [sollet.io](https://www.sollet.io) for a demo. See [sollet.io](https://www.sollet.io) or the [Sollet Chrome Extension](https://chrome.google.com/webstore/detail/sollet/fhmfendgdocmcbmfikdcogofphimnkno) for a demo.
Wallet keys are stored in `localStorage`, optionally encrypted by a password. Wallet keys are stored in `localStorage`, optionally encrypted by a password.

View File

@ -1,4 +1,5 @@
const responseHandlers = new Map(); const responseHandlers = new Map();
let unlockedMnemonic = '';
function launchPopup(message, sender, sendResponse) { function launchPopup(message, sender, sendResponse) {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
@ -66,5 +67,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const responseHandler = responseHandlers.get(message.data.id); const responseHandler = responseHandlers.get(message.data.id);
responseHandlers.delete(message.data.id); responseHandlers.delete(message.data.id);
responseHandler(message.data); responseHandler(message.data);
} else if (message.channel === 'sollet_extension_mnemonic_channel') {
if (message.method === 'set') {
unlockedMnemonic = message.data;
} else if (message.method === 'get') {
sendResponse(unlockedMnemonic);
}
} }
}); });

View File

@ -167,7 +167,7 @@ export default function AddTokenDialog({ open, onClose }) {
</React.Fragment> </React.Fragment>
) : tab === 'popular' ? ( ) : tab === 'popular' ? (
<List disablePadding> <List disablePadding>
{popularTokens.map((tokenInfo) => ( {popularTokens.filter(tokenInfo => tokenInfo.address).map((tokenInfo) => (
<TokenListItem <TokenListItem
key={tokenInfo.address} key={tokenInfo.address}
tokenInfo={tokenInfo} tokenInfo={tokenInfo}

View File

@ -33,6 +33,7 @@ import Tooltip from '@material-ui/core/Tooltip';
import EditIcon from '@material-ui/icons/Edit'; import EditIcon from '@material-ui/icons/Edit';
import MergeType from '@material-ui/icons/MergeType'; import MergeType from '@material-ui/icons/MergeType';
import SortIcon from '@material-ui/icons/Sort'; import SortIcon from '@material-ui/icons/Sort';
import DeleteIcon from '@material-ui/icons/Delete';
import AddTokenDialog from './AddTokenDialog'; import AddTokenDialog from './AddTokenDialog';
import ExportAccountDialog from './ExportAccountDialog'; import ExportAccountDialog from './ExportAccountDialog';
import SendDialog from './SendDialog'; import SendDialog from './SendDialog';
@ -539,7 +540,7 @@ function BalanceListItemDetails({
return <LoadingIndicator delay={0} />; return <LoadingIndicator delay={0} />;
} }
let { mint, tokenName, tokenSymbol, owner } = balanceInfo; let { mint, tokenName, tokenSymbol, owner, amount } = balanceInfo;
// Only show the export UI for the native SOL coin. // Only show the export UI for the native SOL coin.
const exportNeedsDisplay = const exportNeedsDisplay =
@ -664,6 +665,18 @@ function BalanceListItemDetails({
> >
Send Send
</Button> </Button>
{localStorage.getItem('warning-close-account') && mint && amount === 0 ? (
<Button
variant="outlined"
color="secondary"
size="small"
startIcon={<DeleteIcon />}
onClick={() => setCloseTokenAccountDialogOpen(true)}
>
Delete
</Button>
) : null}
</div> </div>
{additionalInfo} {additionalInfo}
</div> </div>

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import DialogForm from './DialogForm'; import DialogForm from './DialogForm';
import { forgetWallet } from '../utils/wallet-seed'; import { forgetWallet, normalizeMnemonic, useUnlockedMnemonicAndSeed } from '../utils/wallet-seed';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import { DialogContentText } from '@material-ui/core'; import { DialogContentText } from '@material-ui/core';
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from '@material-ui/core/DialogActions';
@ -8,7 +8,8 @@ import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
export default function DeleteMnemonicDialog({ open, onClose }) { export default function DeleteMnemonicDialog({ open, onClose }) {
const [deleteCheck, setDeleteCheck] = useState(''); const [seedCheck, setSeedCheck] = useState('');
const [mnemKey] = useUnlockedMnemonicAndSeed();
return ( return (
<> <>
<DialogForm <DialogForm
@ -35,16 +36,17 @@ export default function DeleteMnemonicDialog({ open, onClose }) {
<br /> <br />
<strong> <strong>
To prevent loss of funds, please ensure you have the seed phrase To prevent loss of funds, please ensure you have the seed phrase
and the private key for all current accounts. and the private key for all current accounts. You can view it by selecting
"Export Mnemonic" in the user menu.
</strong> </strong>
</div> </div>
<TextField <TextField
label={`Please type "delete" to confirm`} label={`Please type your seed phrase to confirm`}
fullWidth fullWidth
variant="outlined" variant="outlined"
margin="normal" margin="normal"
value={deleteCheck} value={seedCheck}
onChange={(e) => setDeleteCheck(e.target.value.trim())} onChange={(e) => setSeedCheck(e.target.value)}
/> />
</DialogContentText> </DialogContentText>
<DialogActions> <DialogActions>
@ -52,7 +54,7 @@ export default function DeleteMnemonicDialog({ open, onClose }) {
<Button <Button
type="submit" type="submit"
color="secondary" color="secondary"
disabled={deleteCheck !== 'delete'} disabled={normalizeMnemonic(seedCheck) !== mnemKey.mnemonic}
> >
Delete Delete
</Button> </Button>

View File

@ -8,7 +8,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
import Switch from '@material-ui/core/Switch'; import Switch from '@material-ui/core/Switch';
import DialogForm from './DialogForm'; import DialogForm from './DialogForm';
import { useWallet } from '../utils/wallet'; import { useWallet } from '../utils/wallet';
import { getUnlockedMnemonicAndSeed } from '../utils/wallet-seed'; import { useUnlockedMnemonicAndSeed } from '../utils/wallet-seed';
export default function ExportAccountDialog({ open, onClose }) { export default function ExportAccountDialog({ open, onClose }) {
const wallet = useWallet(); const wallet = useWallet();
@ -45,7 +45,7 @@ export default function ExportAccountDialog({ open, onClose }) {
export function ExportMnemonicDialog({ open, onClose }) { export function ExportMnemonicDialog({ open, onClose }) {
const [isHidden, setIsHidden] = useState(true); const [isHidden, setIsHidden] = useState(true);
const mnemKey = getUnlockedMnemonicAndSeed(); const [mnemKey] = useUnlockedMnemonicAndSeed();
return ( return (
<DialogForm open={open} onClose={onClose} fullWidth> <DialogForm open={open} onClose={onClose} fullWidth>
<DialogTitle>Export mnemonic</DialogTitle> <DialogTitle>Export mnemonic</DialogTitle>

View File

@ -115,6 +115,7 @@ export default function MergeAccountsDialog({ open, onClose }) {
assocTokAddr, assocTokAddr,
mintGroup, mintGroup,
mint, mint,
tokenInfo.decimals,
wallet, wallet,
connection, connection,
enqueueSnackbar, enqueueSnackbar,
@ -242,11 +243,11 @@ async function mergeMint(
assocTokAddr, assocTokAddr,
mintAccountSet, mintAccountSet,
mint, mint,
decimals,
wallet, wallet,
connection, connection,
enqueueSnackbar, enqueueSnackbar,
) { ) {
console.log('mint', mint, mint.toString());
if (mintAccountSet.length === 0) { if (mintAccountSet.length === 0) {
return; return;
} }
@ -292,6 +293,7 @@ async function mergeMint(
associatedTokenAccount, associatedTokenAccount,
tokenAccount.account.amount, tokenAccount.account.amount,
mint, mint,
decimals,
); );
} }
} }

View File

@ -241,6 +241,7 @@ function NetworkSelector() {
function WalletSelector() { function WalletSelector() {
const { const {
accounts, accounts,
derivedAccounts,
hardwareWalletAccount, hardwareWalletAccount,
setHardwareWalletAccount, setHardwareWalletAccount,
setWalletSelector, setWalletSelector,
@ -290,7 +291,7 @@ function WalletSelector() {
onAdd={({ name, importedAccount }) => { onAdd={({ name, importedAccount }) => {
addAccount({ name, importedAccount }); addAccount({ name, importedAccount });
setWalletSelector({ setWalletSelector({
walletIndex: importedAccount ? undefined : accounts.length, walletIndex: importedAccount ? undefined : derivedAccounts.length,
importedPubkey: importedAccount importedPubkey: importedAccount
? importedAccount.publicKey.toString() ? importedAccount.publicKey.toString()
: undefined, : undefined,

View File

@ -81,6 +81,13 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
<Tab label="SPL USDT" key="wusdtToSplUsdt" value="wusdtToSplUsdt" />, <Tab label="SPL USDT" key="wusdtToSplUsdt" value="wusdtToSplUsdt" />,
<Tab label="ERC20 USDT" key="swap" value="swap" />, <Tab label="ERC20 USDT" key="swap" value="swap" />,
]; ];
} else if (localStorage.getItem('sollet-private') && mint?.equals(USDC_MINT)) {
return [
<Tab label="SPL USDC" key="spl" value="spl" />,
<Tab label="SPL WUSDC" key="usdcToSplWUsdc" value="usdcToSplWUsdc" />,
wormholeTab,
<Tab label="ERC20 USDC" key="swap" value="swap" />,
];
} else { } else {
return [ return [
<Tab label={`SPL ${swapCoinInfo.ticker}`} key="spl" value="spl" />, <Tab label={`SPL ${swapCoinInfo.ticker}`} key="spl" value="spl" />,
@ -164,6 +171,16 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
swapCoinInfo={swapCoinInfo} swapCoinInfo={swapCoinInfo}
onSubmitRef={onSubmitRef} onSubmitRef={onSubmitRef}
/> />
) : tab === 'usdcToSplWUsdc' ? (
<SendSwapDialog
key={tab}
onClose={onClose}
publicKey={publicKey}
balanceInfo={balanceInfo}
swapCoinInfo={swapCoinInfo}
onSubmitRef={onSubmitRef}
usdcToSplWUsdc
/>
) : ( ) : (
<SendSwapDialog <SendSwapDialog
key={tab} key={tab}
@ -263,6 +280,7 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
new PublicKey(destinationAddress), new PublicKey(destinationAddress),
amount, amount,
balanceInfo.mint, balanceInfo.mint,
decimals,
null, null,
overrideDestinationCheck, overrideDestinationCheck,
); );
@ -313,6 +331,7 @@ function SendSwapDialog({
ethAccount, ethAccount,
wusdcToSplUsdc = false, wusdcToSplUsdc = false,
wusdtToSplUsdt = false, wusdtToSplUsdt = false,
usdcToSplWUsdc = false,
onSubmitRef, onSubmitRef,
}) { }) {
const wallet = useWallet(); const wallet = useWallet();
@ -328,7 +347,7 @@ function SendSwapDialog({
const { tokenName, decimals, mint } = balanceInfo; const { tokenName, decimals, mint } = balanceInfo;
const blockchain = const blockchain =
wusdcToSplUsdc || wusdtToSplUsdt wusdcToSplUsdc || wusdtToSplUsdt || usdcToSplWUsdc
? 'sol' ? 'sol'
: swapCoinInfo.blockchain === 'sol' : swapCoinInfo.blockchain === 'sol'
? 'eth' ? 'eth'
@ -366,11 +385,16 @@ function SendSwapDialog({
let splUsdtWalletAddress = useWalletAddressForMint( let splUsdtWalletAddress = useWalletAddressForMint(
wusdtToSplUsdt ? USDT_MINT : null, wusdtToSplUsdt ? USDT_MINT : null,
); );
let splWUsdcWalletAddress = useWalletAddressForMint(
usdcToSplWUsdc ? WUSDC_MINT : null,
);
useEffect(() => { useEffect(() => {
if (wusdcToSplUsdc && splUsdcWalletAddress) { if (wusdcToSplUsdc && splUsdcWalletAddress) {
setDestinationAddress(splUsdcWalletAddress); setDestinationAddress(splUsdcWalletAddress);
} else if (wusdtToSplUsdt && splUsdtWalletAddress) { } else if (wusdtToSplUsdt && splUsdtWalletAddress) {
setDestinationAddress(splUsdtWalletAddress); setDestinationAddress(splUsdtWalletAddress);
} else if (usdcToSplWUsdc && splWUsdcWalletAddress) {
setDestinationAddress(splWUsdcWalletAddress);
} }
}, [ }, [
setDestinationAddress, setDestinationAddress,
@ -378,6 +402,8 @@ function SendSwapDialog({
splUsdcWalletAddress, splUsdcWalletAddress,
wusdtToSplUsdt, wusdtToSplUsdt,
splUsdtWalletAddress, splUsdtWalletAddress,
usdcToSplWUsdc,
splWUsdcWalletAddress,
]); ]);
async function makeTransaction() { async function makeTransaction() {
@ -397,6 +423,11 @@ function SendSwapDialog({
} }
if (mint?.equals(WUSDC_MINT)) { if (mint?.equals(WUSDC_MINT)) {
params.wusdcToUsdc = true; params.wusdcToUsdc = true;
} else if (mint?.equals(USDC_MINT)) {
if (usdcToSplWUsdc) {
params.usdcToWUsdc = true;
params.coin = WUSDC_MINT.toString();
}
} else if (mint?.equals(WUSDT_MINT)) { } else if (mint?.equals(WUSDT_MINT)) {
params.wusdtToUsdt = true; params.wusdtToUsdt = true;
} }
@ -409,6 +440,7 @@ function SendSwapDialog({
new PublicKey(swapInfo.address), new PublicKey(swapInfo.address),
amount, amount,
balanceInfo.mint, balanceInfo.mint,
decimals,
swapInfo.memo, swapInfo.memo,
); );
} }

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
generateMnemonicAndSeed, generateMnemonicAndSeed,
hasLockedMnemonicAndSeed, useHasLockedMnemonicAndSeed,
loadMnemonicAndSeed, loadMnemonicAndSeed,
mnemonicToSeed, mnemonicToSeed,
storeMnemonicAndSeed, storeMnemonicAndSeed,
normalizeMnemonic,
} from '../utils/wallet-seed'; } from '../utils/wallet-seed';
import { import {
getAccountFromSeed, getAccountFromSeed,
@ -15,7 +16,7 @@ import LoadingIndicator from '../components/LoadingIndicator';
import { BalanceListItem } from '../components/BalancesList.js'; import { BalanceListItem } from '../components/BalancesList.js';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import { Typography } from '@material-ui/core'; import { DialogActions, DialogContentText, DialogTitle, Typography } from '@material-ui/core';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import Checkbox from '@material-ui/core/Checkbox'; import Checkbox from '@material-ui/core/Checkbox';
import FormControl from '@material-ui/core/FormControl'; import FormControl from '@material-ui/core/FormControl';
@ -27,16 +28,23 @@ import MenuItem from '@material-ui/core/MenuItem';
import { useCallAsync } from '../utils/notifications'; import { useCallAsync } from '../utils/notifications';
import Link from '@material-ui/core/Link'; import Link from '@material-ui/core/Link';
import { validateMnemonic } from 'bip39'; import { validateMnemonic } from 'bip39';
import DialogForm from '../components/DialogForm';
export default function LoginPage() { export default function LoginPage() {
const [restore, setRestore] = useState(false); const [restore, setRestore] = useState(false);
const [hasLockedMnemonicAndSeed, loading] = useHasLockedMnemonicAndSeed();
if (loading) {
return null;
}
return ( return (
<Container maxWidth="sm"> <Container maxWidth="sm">
{restore ? ( {restore ? (
<RestoreWalletForm goBack={() => setRestore(false)} /> <RestoreWalletForm goBack={() => setRestore(false)} />
) : ( ) : (
<> <>
{hasLockedMnemonicAndSeed() ? <LoginForm /> : <CreateWalletForm />} {hasLockedMnemonicAndSeed ? <LoginForm /> : <CreateWalletForm />}
<br /> <br />
<Link style={{ cursor: 'pointer' }} onClick={() => setRestore(true)}> <Link style={{ cursor: 'pointer' }} onClick={() => setRestore(true)}>
Restore existing wallet Restore existing wallet
@ -91,8 +99,11 @@ function CreateWalletForm() {
function SeedWordsForm({ mnemonicAndSeed, goForward }) { function SeedWordsForm({ mnemonicAndSeed, goForward }) {
const [confirmed, setConfirmed] = useState(false); const [confirmed, setConfirmed] = useState(false);
const [showDialog, setShowDialog] = useState(false);
const [seedCheck, setSeedCheck] = useState('');
return ( return (
<>
<Card> <Card>
<CardContent> <CardContent>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
@ -140,11 +151,48 @@ function SeedWordsForm({ mnemonicAndSeed, goForward }) {
/> />
</CardContent> </CardContent>
<CardActions style={{ justifyContent: 'flex-end' }}> <CardActions style={{ justifyContent: 'flex-end' }}>
<Button color="primary" disabled={!confirmed} onClick={goForward}> <Button color="primary" disabled={!confirmed} onClick={() => setShowDialog(true)}>
Continue Continue
</Button> </Button>
</CardActions> </CardActions>
</Card> </Card>
<DialogForm
open={showDialog}
onClose={() => setShowDialog(false)}
onSubmit={goForward}
fullWidth
>
<DialogTitle>{'Confirm Mnemonic'}</DialogTitle>
<DialogContentText style={{ margin: 20 }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
Please re-enter your seed phrase to confirm that you have saved it.
</div>
<TextField
label={`Please type your seed phrase to confirm`}
fullWidth
variant="outlined"
margin="normal"
value={seedCheck}
onChange={(e) => setSeedCheck(e.target.value)}
/>
</DialogContentText>
<DialogActions>
<Button onClick={() => setShowDialog(false)}>Close</Button>
<Button
type="submit"
color="secondary"
disabled={normalizeMnemonic(seedCheck) !== mnemonicAndSeed?.mnemonic}
>
Continue
</Button>
</DialogActions>
</DialogForm>
</>
); );
} }
@ -205,12 +253,21 @@ function LoginForm() {
const [stayLoggedIn, setStayLoggedIn] = useState(false); const [stayLoggedIn, setStayLoggedIn] = useState(false);
const callAsync = useCallAsync(); const callAsync = useCallAsync();
function submit() { const submit = () => {
callAsync(loadMnemonicAndSeed(password, stayLoggedIn), { callAsync(loadMnemonicAndSeed(password, stayLoggedIn), {
progressMessage: 'Unlocking wallet...', progressMessage: 'Unlocking wallet...',
successMessage: 'Wallet unlocked', successMessage: 'Wallet unlocked',
}); });
} }
const submitOnEnter = (e) => {
if (e.code === "Enter" || e.code === "NumpadEnter") {
e.preventDefault();
e.stopPropagation();
submit();
}
}
const setPasswordOnChange = (e) => setPassword(e.target.value);
const toggleStayLoggedIn = (e) => setStayLoggedIn(e.target.checked);
return ( return (
<Card> <Card>
@ -226,13 +283,14 @@ function LoginForm() {
type="password" type="password"
autoComplete="current-password" autoComplete="current-password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={setPasswordOnChange}
onKeyDown={submitOnEnter}
/> />
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={stayLoggedIn} checked={stayLoggedIn}
onChange={(e) => setStayLoggedIn(e.target.checked)} onChange={toggleStayLoggedIn}
/> />
} }
label="Keep wallet unlocked" label="Keep wallet unlocked"
@ -248,11 +306,13 @@ function LoginForm() {
} }
function RestoreWalletForm({ goBack }) { function RestoreWalletForm({ goBack }) {
const [mnemonic, setMnemonic] = useState(''); const [rawMnemonic, setRawMnemonic] = useState('');
const [seed, setSeed] = useState(''); const [seed, setSeed] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState(''); const [passwordConfirm, setPasswordConfirm] = useState('');
const [next, setNext] = useState(false); const [next, setNext] = useState(false);
const mnemonic = normalizeMnemonic(rawMnemonic);
const isNextBtnEnabled = const isNextBtnEnabled =
password === passwordConfirm && validateMnemonic(mnemonic); password === passwordConfirm && validateMnemonic(mnemonic);
@ -287,8 +347,8 @@ function RestoreWalletForm({ goBack }) {
rows={3} rows={3}
margin="normal" margin="normal"
label="Seed Words" label="Seed Words"
value={mnemonic} value={rawMnemonic}
onChange={(e) => setMnemonic(e.target.value)} onChange={(e) => setRawMnemonic(e.target.value)}
/> />
<TextField <TextField
variant="outlined" variant="outlined"

View File

@ -22,6 +22,7 @@ export async function swapApiRequest(
headers['Content-Type'] = 'application/json'; headers['Content-Type'] = 'application/json';
params.body = JSON.stringify(body); params.body = JSON.stringify(body);
} }
let resp = await fetch(`https://swap.sollet.io/api/${path}`, params); let resp = await fetch(`https://swap.sollet.io/api/${path}`, params);
return await handleSwapApiResponse(resp, ignoreUserErrors); return await handleSwapApiResponse(resp, ignoreUserErrors);
} }

View File

@ -14,7 +14,7 @@ import {
memoInstruction, memoInstruction,
mintTo, mintTo,
TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID,
transfer, transferChecked,
} from './instructions'; } from './instructions';
import { import {
ACCOUNT_LAYOUT, ACCOUNT_LAYOUT,
@ -300,6 +300,7 @@ export async function transferTokens({
amount, amount,
memo, memo,
mint, mint,
decimals,
overrideDestinationCheck, overrideDestinationCheck,
}) { }) {
const destinationAccountInfo = await connection.getAccountInfo( const destinationAccountInfo = await connection.getAccountInfo(
@ -312,6 +313,8 @@ export async function transferTokens({
return await transferBetweenSplTokenAccounts({ return await transferBetweenSplTokenAccounts({
connection, connection,
owner, owner,
mint,
decimals,
sourcePublicKey, sourcePublicKey,
destinationPublicKey, destinationPublicKey,
amount, amount,
@ -339,6 +342,8 @@ export async function transferTokens({
return await transferBetweenSplTokenAccounts({ return await transferBetweenSplTokenAccounts({
connection, connection,
owner, owner,
mint,
decimals,
sourcePublicKey, sourcePublicKey,
destinationPublicKey: destinationSplTokenAccount.publicKey, destinationPublicKey: destinationSplTokenAccount.publicKey,
amount, amount,
@ -353,44 +358,24 @@ export async function transferTokens({
amount, amount,
memo, memo,
mint, mint,
decimals,
}); });
} }
// SPL tokens only.
export async function transferAndClose({
connection,
owner,
sourcePublicKey,
destinationPublicKey,
amount,
}) {
const tx = createTransferBetweenSplTokenAccountsInstruction({
ownerPublicKey: owner.publicKey,
sourcePublicKey,
destinationPublicKey,
amount,
});
tx.add(
closeAccount({
source: sourcePublicKey,
destination: owner.publicKey,
owner: owner.publicKey,
}),
);
let signers = [];
return await signAndSendTransaction(connection, tx, owner, signers);
}
function createTransferBetweenSplTokenAccountsInstruction({ function createTransferBetweenSplTokenAccountsInstruction({
ownerPublicKey, ownerPublicKey,
mint,
decimals,
sourcePublicKey, sourcePublicKey,
destinationPublicKey, destinationPublicKey,
amount, amount,
memo, memo,
}) { }) {
let transaction = new Transaction().add( let transaction = new Transaction().add(
transfer({ transferChecked({
source: sourcePublicKey, source: sourcePublicKey,
mint,
decimals,
destination: destinationPublicKey, destination: destinationPublicKey,
owner: ownerPublicKey, owner: ownerPublicKey,
amount, amount,
@ -405,6 +390,8 @@ function createTransferBetweenSplTokenAccountsInstruction({
async function transferBetweenSplTokenAccounts({ async function transferBetweenSplTokenAccounts({
connection, connection,
owner, owner,
mint,
decimals,
sourcePublicKey, sourcePublicKey,
destinationPublicKey, destinationPublicKey,
amount, amount,
@ -412,6 +399,8 @@ async function transferBetweenSplTokenAccounts({
}) { }) {
const transaction = createTransferBetweenSplTokenAccountsInstruction({ const transaction = createTransferBetweenSplTokenAccountsInstruction({
ownerPublicKey: owner.publicKey, ownerPublicKey: owner.publicKey,
mint,
decimals,
sourcePublicKey, sourcePublicKey,
destinationPublicKey, destinationPublicKey,
amount, amount,
@ -429,6 +418,7 @@ async function createAndTransferToAccount({
amount, amount,
memo, memo,
mint, mint,
decimals,
}) { }) {
const [ const [
createAccountInstruction, createAccountInstruction,
@ -449,6 +439,8 @@ async function createAndTransferToAccount({
const transferBetweenAccountsTxn = createTransferBetweenSplTokenAccountsInstruction( const transferBetweenAccountsTxn = createTransferBetweenSplTokenAccountsInstruction(
{ {
ownerPublicKey: owner.publicKey, ownerPublicKey: owner.publicKey,
mint,
decimals,
sourcePublicKey, sourcePublicKey,
destinationPublicKey: newAddress, destinationPublicKey: newAddress,
amount, amount,

View File

@ -29,11 +29,6 @@ LAYOUT.addVariant(
'initializeMint', 'initializeMint',
); );
LAYOUT.addVariant(1, BufferLayout.struct([]), 'initializeAccount'); LAYOUT.addVariant(1, BufferLayout.struct([]), 'initializeAccount');
LAYOUT.addVariant(
3,
BufferLayout.struct([BufferLayout.nu64('amount')]),
'transfer',
);
LAYOUT.addVariant( LAYOUT.addVariant(
7, 7,
BufferLayout.struct([BufferLayout.nu64('amount')]), BufferLayout.struct([BufferLayout.nu64('amount')]),
@ -45,6 +40,11 @@ LAYOUT.addVariant(
'burn', 'burn',
); );
LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount'); LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount');
LAYOUT.addVariant(
12,
BufferLayout.struct([BufferLayout.nu64('amount'), BufferLayout.u8('decimals')]),
'transferChecked',
);
const instructionMaxSpan = Math.max( const instructionMaxSpan = Math.max(
...Object.values(LAYOUT.registry).map((r) => r.span), ...Object.values(LAYOUT.registry).map((r) => r.span),
@ -96,16 +96,17 @@ export function initializeAccount({ account, mint, owner }) {
}); });
} }
export function transfer({ source, destination, amount, owner }) { export function transferChecked({ source, mint, destination, amount, decimals, owner }) {
let keys = [ let keys = [
{ pubkey: source, isSigner: false, isWritable: true }, { pubkey: source, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: destination, isSigner: false, isWritable: true }, { pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false }, { pubkey: owner, isSigner: true, isWritable: false },
]; ];
return new TransactionInstruction({ return new TransactionInstruction({
keys, keys,
data: encodeTokenInstructionData({ data: encodeTokenInstructionData({
transfer: { amount }, transferChecked: { amount, decimals },
}), }),
programId: TOKEN_PROGRAM_ID, programId: TOKEN_PROGRAM_ID,
}); });

View File

@ -239,6 +239,27 @@ const POPULAR_TOKENS = {
icon: icon:
'https://raw.githubusercontent.com/nathanielparke/awesome-serum-markets/master/icons/oxy.svg', 'https://raw.githubusercontent.com/nathanielparke/awesome-serum-markets/master/icons/oxy.svg',
}, },
{
tokenSymbol: 'COPE',
mintAddress: '3K6rftdAaQYMPunrtNRHgnK2UAtjm2JwyT2oCiTDouYE',
tokenName: 'COPE',
icon:
'https://cdn.jsdelivr.net/gh/solana-labs/token-list/assets/mainnet/3K6rftdAaQYMPunrtNRHgnK2UAtjm2JwyT2oCiTDouYE/logo.jpg',
},
{
tokenSymbol: 'BRZ',
mintAddress: 'FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD',
tokenName: 'Brazilian Digital Token',
icon:
'https://cdn.jsdelivr.net/gh/solana-labs/explorer/public/tokens/brz.png',
},
{
tokenSymbol: 'STEP',
mintAddress: 'StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT',
tokenName: 'Step',
icon:
'https://cdn.jsdelivr.net/gh/solana-labs/token-list/assets/mainnet/StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT/logo.png',
},
], ],
}; };

View File

@ -4,6 +4,11 @@ import * as bip32 from 'bip32';
import bs58 from 'bs58'; import bs58 from 'bs58';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { isExtension } from './utils'; import { isExtension } from './utils';
import { useEffect, useState } from 'react';
export function normalizeMnemonic(mnemonic) {
return mnemonic.trim().split(/\s+/g).join(" ");
}
export async function generateMnemonicAndSeed() { export async function generateMnemonicAndSeed() {
const bip39 = await import('bip39'); const bip39 = await import('bip39');
@ -21,38 +26,75 @@ export async function mnemonicToSeed(mnemonic) {
return Buffer.from(seed).toString('hex'); return Buffer.from(seed).toString('hex');
} }
let unlockedMnemonicAndSeed = (() => { async function getExtensionUnlockedMnemonic() {
if (!isExtension) {
return null;
}
return new Promise((resolve) => {
chrome.runtime.sendMessage({
channel: 'sollet_extension_mnemonic_channel',
method: 'get',
}, resolve);
})
}
const EMPTY_MNEMONIC = {
mnemonic: null,
seed: null,
importsEncryptionKey: null,
derivationPath: null,
};
let unlockedMnemonicAndSeed = (async () => {
const unlockedExpiration = localStorage.getItem('unlockedExpiration'); const unlockedExpiration = localStorage.getItem('unlockedExpiration');
// Left here to clean up stored mnemonics from previous method
if (unlockedExpiration && Number(unlockedExpiration) < Date.now()) { if (unlockedExpiration && Number(unlockedExpiration) < Date.now()) {
localStorage.removeItem('unlocked'); localStorage.removeItem('unlocked');
localStorage.removeItem('unlockedExpiration'); localStorage.removeItem('unlockedExpiration');
} }
const stored = JSON.parse( const stored = JSON.parse(
(await getExtensionUnlockedMnemonic()) ||
sessionStorage.getItem('unlocked') || sessionStorage.getItem('unlocked') ||
localStorage.getItem('unlocked') || localStorage.getItem('unlocked') ||
'null', 'null',
); );
if (stored === null) { if (stored === null) {
return { return EMPTY_MNEMONIC;
mnemonic: null,
seed: null,
importsEncryptionKey: null,
derivationPath: null,
};
} }
return { return {
importsEncryptionKey: deriveImportsEncryptionKey(stored.seed), importsEncryptionKey: deriveImportsEncryptionKey(stored.seed),
...stored, ...stored,
}; };
})(); })();
export const walletSeedChanged = new EventEmitter(); export const walletSeedChanged = new EventEmitter();
export function getUnlockedMnemonicAndSeed() { export function getUnlockedMnemonicAndSeed() {
return unlockedMnemonicAndSeed; return unlockedMnemonicAndSeed;
} }
export function hasLockedMnemonicAndSeed() { // returns [mnemonic, loading]
return !!localStorage.getItem('locked'); export function useUnlockedMnemonicAndSeed() {
const [currentUnlockedMnemonic, setCurrentUnlockedMnemonic] = useState(null);
useEffect(() => {
walletSeedChanged.addListener('change', setCurrentUnlockedMnemonic);
unlockedMnemonicAndSeed.then(setCurrentUnlockedMnemonic);
return () => {
walletSeedChanged.removeListener('change', setCurrentUnlockedMnemonic);
}
}, []);
return !currentUnlockedMnemonic
? [EMPTY_MNEMONIC, true]
: [currentUnlockedMnemonic, false];
}
export function useHasLockedMnemonicAndSeed() {
const [unlockedMnemonic, loading] = useUnlockedMnemonicAndSeed();
return [!unlockedMnemonic.seed && !!localStorage.getItem('locked'), loading];
} }
function setUnlockedMnemonicAndSeed( function setUnlockedMnemonicAndSeed(
@ -61,13 +103,14 @@ function setUnlockedMnemonicAndSeed(
importsEncryptionKey, importsEncryptionKey,
derivationPath, derivationPath,
) { ) {
unlockedMnemonicAndSeed = { const data = {
mnemonic, mnemonic,
seed, seed,
importsEncryptionKey, importsEncryptionKey,
derivationPath, derivationPath,
}; };
walletSeedChanged.emit('change', unlockedMnemonicAndSeed); unlockedMnemonicAndSeed = Promise.resolve(data);
walletSeedChanged.emit('change', data);
} }
export async function storeMnemonicAndSeed( export async function storeMnemonicAndSeed(
@ -97,11 +140,17 @@ export async function storeMnemonicAndSeed(
}), }),
); );
localStorage.removeItem('unlocked'); localStorage.removeItem('unlocked');
sessionStorage.removeItem('unlocked');
} else { } else {
localStorage.setItem('unlocked', plaintext); localStorage.setItem('unlocked', plaintext);
localStorage.removeItem('locked'); localStorage.removeItem('locked');
}
sessionStorage.removeItem('unlocked'); sessionStorage.removeItem('unlocked');
if (isExtension) {
chrome.runtime.sendMessage({
channel: 'sollet_extension_mnemonic_channel',
method: 'set',
data: '',
});
} }
const importsEncryptionKey = deriveImportsEncryptionKey(seed); const importsEncryptionKey = deriveImportsEncryptionKey(seed);
setUnlockedMnemonicAndSeed( setUnlockedMnemonicAndSeed(
@ -132,12 +181,15 @@ export async function loadMnemonicAndSeed(password, stayLoggedIn) {
const { mnemonic, seed, derivationPath } = JSON.parse(decodedPlaintext); const { mnemonic, seed, derivationPath } = JSON.parse(decodedPlaintext);
if (stayLoggedIn) { if (stayLoggedIn) {
if (isExtension) { if (isExtension) {
const expireMs = 1000 * 60 * 60 * 24; chrome.runtime.sendMessage({
localStorage.setItem('unlockedExpiration', Date.now() + expireMs); channel: 'sollet_extension_mnemonic_channel',
localStorage.setItem('unlocked', decodedPlaintext); method: 'set',
} data: decodedPlaintext,
});
} else {
sessionStorage.setItem('unlocked', decodedPlaintext); sessionStorage.setItem('unlocked', decodedPlaintext);
} }
}
const importsEncryptionKey = deriveImportsEncryptionKey(seed); const importsEncryptionKey = deriveImportsEncryptionKey(seed);
setUnlockedMnemonicAndSeed( setUnlockedMnemonicAndSeed(
mnemonic, mnemonic,
@ -175,6 +227,13 @@ function deriveImportsEncryptionKey(seed) {
export function forgetWallet() { export function forgetWallet() {
localStorage.clear(); localStorage.clear();
sessionStorage.removeItem('unlocked'); sessionStorage.removeItem('unlocked');
if (isExtension) {
chrome.runtime.sendMessage({
channel: 'sollet_extension_mnemonic_channel',
method: 'set',
data: '',
});
}
unlockedMnemonicAndSeed = { unlockedMnemonicAndSeed = {
mnemonic: null, mnemonic: null,
seed: null, seed: null,

View File

@ -14,7 +14,6 @@ import {
getOwnedTokenAccounts, getOwnedTokenAccounts,
nativeTransfer, nativeTransfer,
transferTokens, transferTokens,
transferAndClose,
} from './tokens'; } from './tokens';
import { TOKEN_PROGRAM_ID } from './tokens/instructions'; import { TOKEN_PROGRAM_ID } from './tokens/instructions';
import { import {
@ -25,7 +24,7 @@ import {
import { useListener, useLocalStorageState, useRefEqual } from './utils'; import { useListener, useLocalStorageState, useRefEqual } from './utils';
import { useTokenInfo } from './tokens/names'; import { useTokenInfo } from './tokens/names';
import { refreshCache, useAsyncData } from './fetch-loop'; import { refreshCache, useAsyncData } from './fetch-loop';
import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed'; import { useUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed';
import { WalletProviderFactory } from './walletProvider/factory'; import { WalletProviderFactory } from './walletProvider/factory';
import { getAccountFromSeed } from './walletProvider/localStorage'; import { getAccountFromSeed } from './walletProvider/localStorage';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
@ -99,6 +98,7 @@ export class Wallet {
destination, destination,
amount, amount,
mint, mint,
decimals,
memo = null, memo = null,
overrideDestinationCheck = false, overrideDestinationCheck = false,
) => { ) => {
@ -116,6 +116,7 @@ export class Wallet {
amount, amount,
memo, memo,
mint, mint,
decimals,
overrideDestinationCheck, overrideDestinationCheck,
}); });
}; };
@ -133,16 +134,6 @@ export class Wallet {
}); });
}; };
transferAndClose = async (source, destination, amount) => {
return await transferAndClose({
connection: this.connection,
owner: this,
sourcePublicKey: source,
destinationPublicKey: destination,
amount,
});
};
signTransaction = async (transaction) => { signTransaction = async (transaction) => {
return this.provider.signTransaction(transaction); return this.provider.signTransaction(transaction);
}; };
@ -156,12 +147,12 @@ const WalletContext = React.createContext(null);
export function WalletProvider({ children }) { export function WalletProvider({ children }) {
useListener(walletSeedChanged, 'change'); useListener(walletSeedChanged, 'change');
const { const [{
mnemonic, mnemonic,
seed, seed,
importsEncryptionKey, importsEncryptionKey,
derivationPath, derivationPath,
} = getUnlockedMnemonicAndSeed(); }] = useUnlockedMnemonicAndSeed();
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const connection = useConnection(); const connection = useConnection();
const [wallet, setWallet] = useState(); const [wallet, setWallet] = useState();
@ -289,9 +280,9 @@ export function WalletProvider({ children }) {
} }
} }
const accounts = useMemo(() => { const [accounts, derivedAccounts] = useMemo(() => {
if (!seed) { if (!seed) {
return []; return [[], []];
} }
const seedBuffer = Buffer.from(seed, 'hex'); const seedBuffer = Buffer.from(seed, 'hex');
@ -325,7 +316,8 @@ export function WalletProvider({ children }) {
}; };
}); });
return derivedAccounts.concat(importedAccounts); const accounts = derivedAccounts.concat(importedAccounts);
return [accounts, derivedAccounts];
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [seed, walletCount, walletSelector, privateKeyImports, walletNames]); }, [seed, walletCount, walletSelector, privateKeyImports, walletNames]);
@ -358,6 +350,7 @@ export function WalletProvider({ children }) {
privateKeyImports, privateKeyImports,
setPrivateKeyImports, setPrivateKeyImports,
accounts, accounts,
derivedAccounts,
addAccount, addAccount,
setAccountName, setAccountName,
derivationPath, derivationPath,
@ -475,6 +468,7 @@ export function useBalanceInfo(publicKey) {
export function useWalletSelector() { export function useWalletSelector() {
const { const {
accounts, accounts,
derivedAccounts,
addAccount, addAccount,
setWalletSelector, setWalletSelector,
setAccountName, setAccountName,
@ -484,6 +478,7 @@ export function useWalletSelector() {
return { return {
accounts, accounts,
derivedAccounts,
setWalletSelector, setWalletSelector,
addAccount, addAccount,
setAccountName, setAccountName,

View File

@ -40,8 +40,11 @@ function deriveSeed(seed, walletIndex, derivationPath, accountIndex) {
export class LocalStorageWalletProvider { export class LocalStorageWalletProvider {
constructor(args) { constructor(args) {
const { seed } = getUnlockedMnemonicAndSeed();
this.account = args.account; this.account = args.account;
}
init = async () => {
const { seed } = await getUnlockedMnemonicAndSeed();
this.listAddresses = async (walletCount) => { this.listAddresses = async (walletCount) => {
const seedBuffer = Buffer.from(seed, 'hex'); const seedBuffer = Buffer.from(seed, 'hex');
return [...Array(walletCount).keys()].map((walletIndex) => { return [...Array(walletCount).keys()].map((walletIndex) => {
@ -50,9 +53,6 @@ export class LocalStorageWalletProvider {
return { index: walletIndex, address, name }; return { index: walletIndex, address, name };
}); });
}; };
}
init = async () => {
return this; return this;
}; };