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.
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.

View File

@ -1,4 +1,5 @@
const responseHandlers = new Map();
let unlockedMnemonic = '';
function launchPopup(message, sender, sendResponse) {
const searchParams = new URLSearchParams();
@ -66,5 +67,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const responseHandler = responseHandlers.get(message.data.id);
responseHandlers.delete(message.data.id);
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>
) : tab === 'popular' ? (
<List disablePadding>
{popularTokens.map((tokenInfo) => (
{popularTokens.filter(tokenInfo => tokenInfo.address).map((tokenInfo) => (
<TokenListItem
key={tokenInfo.address}
tokenInfo={tokenInfo}

View File

@ -33,6 +33,7 @@ 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 DeleteIcon from '@material-ui/icons/Delete';
import AddTokenDialog from './AddTokenDialog';
import ExportAccountDialog from './ExportAccountDialog';
import SendDialog from './SendDialog';
@ -539,7 +540,7 @@ function BalanceListItemDetails({
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.
const exportNeedsDisplay =
@ -664,6 +665,18 @@ function BalanceListItemDetails({
>
Send
</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>
{additionalInfo}
</div>

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
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 { DialogContentText } from '@material-ui/core';
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';
export default function DeleteMnemonicDialog({ open, onClose }) {
const [deleteCheck, setDeleteCheck] = useState('');
const [seedCheck, setSeedCheck] = useState('');
const [mnemKey] = useUnlockedMnemonicAndSeed();
return (
<>
<DialogForm
@ -35,16 +36,17 @@ export default function DeleteMnemonicDialog({ open, onClose }) {
<br />
<strong>
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>
</div>
<TextField
label={`Please type "delete" to confirm`}
label={`Please type your seed phrase to confirm`}
fullWidth
variant="outlined"
margin="normal"
value={deleteCheck}
onChange={(e) => setDeleteCheck(e.target.value.trim())}
value={seedCheck}
onChange={(e) => setSeedCheck(e.target.value)}
/>
</DialogContentText>
<DialogActions>
@ -52,7 +54,7 @@ export default function DeleteMnemonicDialog({ open, onClose }) {
<Button
type="submit"
color="secondary"
disabled={deleteCheck !== 'delete'}
disabled={normalizeMnemonic(seedCheck) !== mnemKey.mnemonic}
>
Delete
</Button>

View File

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

View File

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

View File

@ -241,6 +241,7 @@ function NetworkSelector() {
function WalletSelector() {
const {
accounts,
derivedAccounts,
hardwareWalletAccount,
setHardwareWalletAccount,
setWalletSelector,
@ -290,7 +291,7 @@ function WalletSelector() {
onAdd={({ name, importedAccount }) => {
addAccount({ name, importedAccount });
setWalletSelector({
walletIndex: importedAccount ? undefined : accounts.length,
walletIndex: importedAccount ? undefined : derivedAccounts.length,
importedPubkey: importedAccount
? importedAccount.publicKey.toString()
: 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="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 {
return [
<Tab label={`SPL ${swapCoinInfo.ticker}`} key="spl" value="spl" />,
@ -164,6 +171,16 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
swapCoinInfo={swapCoinInfo}
onSubmitRef={onSubmitRef}
/>
) : tab === 'usdcToSplWUsdc' ? (
<SendSwapDialog
key={tab}
onClose={onClose}
publicKey={publicKey}
balanceInfo={balanceInfo}
swapCoinInfo={swapCoinInfo}
onSubmitRef={onSubmitRef}
usdcToSplWUsdc
/>
) : (
<SendSwapDialog
key={tab}
@ -263,6 +280,7 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
new PublicKey(destinationAddress),
amount,
balanceInfo.mint,
decimals,
null,
overrideDestinationCheck,
);
@ -313,6 +331,7 @@ function SendSwapDialog({
ethAccount,
wusdcToSplUsdc = false,
wusdtToSplUsdt = false,
usdcToSplWUsdc = false,
onSubmitRef,
}) {
const wallet = useWallet();
@ -328,7 +347,7 @@ function SendSwapDialog({
const { tokenName, decimals, mint } = balanceInfo;
const blockchain =
wusdcToSplUsdc || wusdtToSplUsdt
wusdcToSplUsdc || wusdtToSplUsdt || usdcToSplWUsdc
? 'sol'
: swapCoinInfo.blockchain === 'sol'
? 'eth'
@ -366,11 +385,16 @@ function SendSwapDialog({
let splUsdtWalletAddress = useWalletAddressForMint(
wusdtToSplUsdt ? USDT_MINT : null,
);
let splWUsdcWalletAddress = useWalletAddressForMint(
usdcToSplWUsdc ? WUSDC_MINT : null,
);
useEffect(() => {
if (wusdcToSplUsdc && splUsdcWalletAddress) {
setDestinationAddress(splUsdcWalletAddress);
} else if (wusdtToSplUsdt && splUsdtWalletAddress) {
setDestinationAddress(splUsdtWalletAddress);
} else if (usdcToSplWUsdc && splWUsdcWalletAddress) {
setDestinationAddress(splWUsdcWalletAddress);
}
}, [
setDestinationAddress,
@ -378,6 +402,8 @@ function SendSwapDialog({
splUsdcWalletAddress,
wusdtToSplUsdt,
splUsdtWalletAddress,
usdcToSplWUsdc,
splWUsdcWalletAddress,
]);
async function makeTransaction() {
@ -397,6 +423,11 @@ function SendSwapDialog({
}
if (mint?.equals(WUSDC_MINT)) {
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)) {
params.wusdtToUsdt = true;
}
@ -409,6 +440,7 @@ function SendSwapDialog({
new PublicKey(swapInfo.address),
amount,
balanceInfo.mint,
decimals,
swapInfo.memo,
);
}

View File

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

View File

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

View File

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

View File

@ -29,11 +29,6 @@ LAYOUT.addVariant(
'initializeMint',
);
LAYOUT.addVariant(1, BufferLayout.struct([]), 'initializeAccount');
LAYOUT.addVariant(
3,
BufferLayout.struct([BufferLayout.nu64('amount')]),
'transfer',
);
LAYOUT.addVariant(
7,
BufferLayout.struct([BufferLayout.nu64('amount')]),
@ -45,6 +40,11 @@ LAYOUT.addVariant(
'burn',
);
LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount');
LAYOUT.addVariant(
12,
BufferLayout.struct([BufferLayout.nu64('amount'), BufferLayout.u8('decimals')]),
'transferChecked',
);
const instructionMaxSpan = Math.max(
...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 = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
transfer: { amount },
transferChecked: { amount, decimals },
}),
programId: TOKEN_PROGRAM_ID,
});

View File

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

View File

@ -14,7 +14,6 @@ import {
getOwnedTokenAccounts,
nativeTransfer,
transferTokens,
transferAndClose,
} from './tokens';
import { TOKEN_PROGRAM_ID } from './tokens/instructions';
import {
@ -25,7 +24,7 @@ import {
import { useListener, useLocalStorageState, useRefEqual } from './utils';
import { useTokenInfo } from './tokens/names';
import { refreshCache, useAsyncData } from './fetch-loop';
import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed';
import { useUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed';
import { WalletProviderFactory } from './walletProvider/factory';
import { getAccountFromSeed } from './walletProvider/localStorage';
import { useSnackbar } from 'notistack';
@ -99,6 +98,7 @@ export class Wallet {
destination,
amount,
mint,
decimals,
memo = null,
overrideDestinationCheck = false,
) => {
@ -116,6 +116,7 @@ export class Wallet {
amount,
memo,
mint,
decimals,
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) => {
return this.provider.signTransaction(transaction);
};
@ -156,12 +147,12 @@ const WalletContext = React.createContext(null);
export function WalletProvider({ children }) {
useListener(walletSeedChanged, 'change');
const {
const [{
mnemonic,
seed,
importsEncryptionKey,
derivationPath,
} = getUnlockedMnemonicAndSeed();
}] = useUnlockedMnemonicAndSeed();
const { enqueueSnackbar } = useSnackbar();
const connection = useConnection();
const [wallet, setWallet] = useState();
@ -289,9 +280,9 @@ export function WalletProvider({ children }) {
}
}
const accounts = useMemo(() => {
const [accounts, derivedAccounts] = useMemo(() => {
if (!seed) {
return [];
return [[], []];
}
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
}, [seed, walletCount, walletSelector, privateKeyImports, walletNames]);
@ -358,6 +350,7 @@ export function WalletProvider({ children }) {
privateKeyImports,
setPrivateKeyImports,
accounts,
derivedAccounts,
addAccount,
setAccountName,
derivationPath,
@ -475,6 +468,7 @@ export function useBalanceInfo(publicKey) {
export function useWalletSelector() {
const {
accounts,
derivedAccounts,
addAccount,
setWalletSelector,
setAccountName,
@ -484,6 +478,7 @@ export function useWalletSelector() {
return {
accounts,
derivedAccounts,
setWalletSelector,
addAccount,
setAccountName,

View File

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