Switch to using getProgramAccounts for finding owned accounts
Use getProgramAccounts instead of relying on deterministically generated keys so we don't need private keys to find accounts
This commit is contained in:
parent
5e1858be1f
commit
5c5d163183
|
@ -12,6 +12,7 @@
|
||||||
"bip32": "^2.0.5",
|
"bip32": "^2.0.5",
|
||||||
"bip39": "^3.0.2",
|
"bip39": "^3.0.2",
|
||||||
"bn.js": "^5.1.2",
|
"bn.js": "^5.1.2",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
"buffer-layout": "^1.2.0",
|
"buffer-layout": "^1.2.0",
|
||||||
"immutable-tuple": "^0.4.10",
|
"immutable-tuple": "^0.4.10",
|
||||||
"notistack": "^0.9.17",
|
"notistack": "^0.9.17",
|
||||||
|
|
12
src/App.js
12
src/App.js
|
@ -31,17 +31,17 @@ function App() {
|
||||||
<Suspense fallback={<LoadingIndicator />}>
|
<Suspense fallback={<LoadingIndicator />}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SnackbarProvider maxSnack={5}>
|
<ConnectionProvider>
|
||||||
<ConnectionProvider>
|
<WalletProvider>
|
||||||
<WalletProvider>
|
<SnackbarProvider maxSnack={5}>
|
||||||
<NavigationFrame>
|
<NavigationFrame>
|
||||||
<Suspense fallback={<LoadingIndicator />}>
|
<Suspense fallback={<LoadingIndicator />}>
|
||||||
<WalletPage />
|
<WalletPage />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</NavigationFrame>
|
</NavigationFrame>
|
||||||
</WalletProvider>
|
</SnackbarProvider>
|
||||||
</ConnectionProvider>
|
</WalletProvider>
|
||||||
</SnackbarProvider>
|
</ConnectionProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,13 +2,13 @@ import React from 'react';
|
||||||
import Container from '@material-ui/core/Container';
|
import Container from '@material-ui/core/Container';
|
||||||
import BalancesList from './components/BalancesList';
|
import BalancesList from './components/BalancesList';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import { useWallet } from './utils/wallet';
|
import { refreshWalletPublicKeys, useWallet } from './utils/wallet';
|
||||||
import { Account, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
import { Account, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
||||||
import { createAndInitializeMint } from './utils/tokens';
|
import { createAndInitializeMint } from './utils/tokens';
|
||||||
import Grid from '@material-ui/core/Grid';
|
import Grid from '@material-ui/core/Grid';
|
||||||
import { refreshAccountInfo, useIsProdNetwork } from './utils/connection';
|
import { refreshAccountInfo, useIsProdNetwork } from './utils/connection';
|
||||||
import { useUpdateTokenName } from './utils/tokens/names';
|
import { useUpdateTokenName } from './utils/tokens/names';
|
||||||
import { sleep } from './utils/utils';
|
import { abbreviateAddress, sleep } from './utils/utils';
|
||||||
import { useCallAsync, useSendTransaction } from './utils/notifications';
|
import { useCallAsync, useSendTransaction } from './utils/notifications';
|
||||||
|
|
||||||
export default function WalletPage() {
|
export default function WalletPage() {
|
||||||
|
@ -34,50 +34,52 @@ function DevnetButtons() {
|
||||||
const updateTokenName = useUpdateTokenName();
|
const updateTokenName = useUpdateTokenName();
|
||||||
const [sendTransaction, sending] = useSendTransaction();
|
const [sendTransaction, sending] = useSendTransaction();
|
||||||
const callAsync = useCallAsync();
|
const callAsync = useCallAsync();
|
||||||
|
|
||||||
|
function requestAirdrop() {
|
||||||
|
callAsync(
|
||||||
|
wallet.connection.requestAirdrop(
|
||||||
|
wallet.account.publicKey,
|
||||||
|
LAMPORTS_PER_SOL,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
await sleep(1000);
|
||||||
|
refreshAccountInfo(wallet.connection, wallet.account.publicKey);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mintTestToken() {
|
||||||
|
let mint = new Account();
|
||||||
|
updateTokenName(
|
||||||
|
mint.publicKey,
|
||||||
|
`Test Token ${abbreviateAddress(mint.publicKey)}`,
|
||||||
|
`TEST${mint.publicKey.toBase58().slice(0, 2)}`,
|
||||||
|
);
|
||||||
|
sendTransaction(
|
||||||
|
createAndInitializeMint({
|
||||||
|
connection: wallet.connection,
|
||||||
|
payer: wallet.account,
|
||||||
|
mint,
|
||||||
|
amount: 1000,
|
||||||
|
decimals: 2,
|
||||||
|
initialAccount: new Account(),
|
||||||
|
mintOwner: wallet.account,
|
||||||
|
}),
|
||||||
|
{ onSuccess: () => refreshWalletPublicKeys(wallet) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Button
|
<Button variant="contained" color="primary" onClick={requestAirdrop}>
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
|
||||||
callAsync(
|
|
||||||
wallet.connection.requestAirdrop(
|
|
||||||
wallet.account.publicKey,
|
|
||||||
LAMPORTS_PER_SOL,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
onSuccess: async () => {
|
|
||||||
await sleep(1000);
|
|
||||||
refreshAccountInfo(wallet.connection, wallet.account.publicKey);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Request Airdrop
|
Request Airdrop
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={mintTestToken}
|
||||||
let mint = new Account();
|
|
||||||
updateTokenName(
|
|
||||||
mint.publicKey,
|
|
||||||
`Test Token ${mint.publicKey.toBase58().slice(0, 4)}`,
|
|
||||||
`TEST${mint.publicKey.toBase58().slice(0, 4)}`,
|
|
||||||
);
|
|
||||||
sendTransaction(
|
|
||||||
createAndInitializeMint({
|
|
||||||
connection: wallet.connection,
|
|
||||||
payer: wallet.account,
|
|
||||||
mint,
|
|
||||||
amount: 1000,
|
|
||||||
decimals: 2,
|
|
||||||
initialAccount: wallet.getAccount(wallet.accountCount),
|
|
||||||
mintOwner: wallet.account,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
style={{ marginLeft: 24 }}
|
style={{ marginLeft: 24 }}
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,11 +6,12 @@ import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
import TextField from '@material-ui/core/TextField';
|
import TextField from '@material-ui/core/TextField';
|
||||||
import DialogForm from './DialogForm';
|
import DialogForm from './DialogForm';
|
||||||
import { useWallet } from '../utils/wallet';
|
import { refreshWalletPublicKeys, useWallet } from '../utils/wallet';
|
||||||
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
|
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
|
||||||
import { useUpdateTokenName } from '../utils/tokens/names';
|
import { useUpdateTokenName } from '../utils/tokens/names';
|
||||||
import { useAsyncData } from '../utils/fetch-loop';
|
import { useAsyncData } from '../utils/fetch-loop';
|
||||||
import LoadingIndicator from './LoadingIndicator';
|
import LoadingIndicator from './LoadingIndicator';
|
||||||
|
import { useSendTransaction } from '../utils/notifications';
|
||||||
|
|
||||||
const feeFormat = new Intl.NumberFormat(undefined, {
|
const feeFormat = new Intl.NumberFormat(undefined, {
|
||||||
minimumFractionDigits: 6,
|
minimumFractionDigits: 6,
|
||||||
|
@ -28,20 +29,17 @@ export default function AddTokenDialog({ open, onClose }) {
|
||||||
let [mintAddress, setMintAddress] = useState('');
|
let [mintAddress, setMintAddress] = useState('');
|
||||||
let [tokenName, setTokenName] = useState('');
|
let [tokenName, setTokenName] = useState('');
|
||||||
let [tokenSymbol, setTokenSymbol] = useState('');
|
let [tokenSymbol, setTokenSymbol] = useState('');
|
||||||
let [submitting, setSubmitting] = useState(false);
|
let [sendTransaction, sending] = useSendTransaction();
|
||||||
|
|
||||||
async function onSubmit() {
|
function onSubmit() {
|
||||||
setSubmitting(true);
|
let mint = new PublicKey(mintAddress);
|
||||||
try {
|
sendTransaction(wallet.createTokenAccount(mint), {
|
||||||
let mint = new PublicKey(mintAddress);
|
onSuccess: () => {
|
||||||
await wallet.createTokenAccount(mint);
|
updateTokenName(mint, tokenName, tokenSymbol);
|
||||||
updateTokenName(mint, tokenName, tokenSymbol);
|
refreshWalletPublicKeys(wallet);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
},
|
||||||
console.warn(e);
|
});
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -83,7 +81,7 @@ export default function AddTokenDialog({ open, onClose }) {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
<Button type="submit" color="primary" disabled={submitting}>
|
<Button type="submit" color="primary" disabled={sending}>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
|
@ -4,9 +4,10 @@ import ListItem from '@material-ui/core/ListItem';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import Paper from '@material-ui/core/Paper';
|
import Paper from '@material-ui/core/Paper';
|
||||||
import {
|
import {
|
||||||
|
refreshWalletPublicKeys,
|
||||||
useBalanceInfo,
|
useBalanceInfo,
|
||||||
useWallet,
|
useWallet,
|
||||||
useWalletAccountCount,
|
useWalletPublicKeys,
|
||||||
} from '../utils/wallet';
|
} from '../utils/wallet';
|
||||||
import LoadingIndicator from './LoadingIndicator';
|
import LoadingIndicator from './LoadingIndicator';
|
||||||
import Collapse from '@material-ui/core/Collapse';
|
import Collapse from '@material-ui/core/Collapse';
|
||||||
|
@ -27,7 +28,10 @@ import IconButton from '@material-ui/core/IconButton';
|
||||||
import Tooltip from '@material-ui/core/Tooltip';
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
import AddTokenDialog from './AddTokenDialog';
|
import AddTokenDialog from './AddTokenDialog';
|
||||||
import SendDialog from './SendDialog';
|
import SendDialog from './SendDialog';
|
||||||
import { refreshAccountInfo } from '../utils/connection';
|
import {
|
||||||
|
refreshAccountInfo,
|
||||||
|
useSolanaExplorerUrlSuffix,
|
||||||
|
} from '../utils/connection';
|
||||||
|
|
||||||
const balanceFormat = new Intl.NumberFormat(undefined, {
|
const balanceFormat = new Intl.NumberFormat(undefined, {
|
||||||
minimumFractionDigits: 4,
|
minimumFractionDigits: 4,
|
||||||
|
@ -37,7 +41,7 @@ const balanceFormat = new Intl.NumberFormat(undefined, {
|
||||||
|
|
||||||
export default function BalancesList() {
|
export default function BalancesList() {
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
const accountCount = useWalletAccountCount();
|
const [publicKeys, loaded] = useWalletPublicKeys();
|
||||||
const [showAddTokenDialog, setShowAddTokenDialog] = useState(false);
|
const [showAddTokenDialog, setShowAddTokenDialog] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -54,15 +58,12 @@ export default function BalancesList() {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Refresh" arrow>
|
<Tooltip title="Refresh" arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
[...Array(accountCount + 5).keys()].map((i) =>
|
refreshWalletPublicKeys(wallet);
|
||||||
refreshAccountInfo(
|
publicKeys.map((publicKey) =>
|
||||||
wallet.connection,
|
refreshAccountInfo(wallet.connection, publicKey, true),
|
||||||
wallet.getAccount(i).publicKey,
|
);
|
||||||
true,
|
}}
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
style={{ marginRight: -12 }}
|
style={{ marginRight: -12 }}
|
||||||
>
|
>
|
||||||
<RefreshIcon />
|
<RefreshIcon />
|
||||||
|
@ -71,9 +72,10 @@ export default function BalancesList() {
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<List disablePadding>
|
<List disablePadding>
|
||||||
{[...Array(accountCount + 5).keys()].map((i) => (
|
{publicKeys.map((publicKey) => (
|
||||||
<BalanceListItem key={i} index={i} />
|
<BalanceListItem key={publicKey.toBase58()} publicKey={publicKey} />
|
||||||
))}
|
))}
|
||||||
|
{loaded ? null : <LoadingIndicator />}
|
||||||
</List>
|
</List>
|
||||||
<AddTokenDialog
|
<AddTokenDialog
|
||||||
open={showAddTokenDialog}
|
open={showAddTokenDialog}
|
||||||
|
@ -101,34 +103,18 @@ const useStyles = makeStyles((theme) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function BalanceListItem({ index }) {
|
function BalanceListItem({ publicKey }) {
|
||||||
const wallet = useWallet();
|
const balanceInfo = useBalanceInfo(publicKey);
|
||||||
const accountCount = useWalletAccountCount();
|
const urlSuffix = useSolanaExplorerUrlSuffix();
|
||||||
const balanceInfo = useBalanceInfo(index);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||||
|
|
||||||
if (!balanceInfo) {
|
if (!balanceInfo) {
|
||||||
if (index <= accountCount) {
|
return <LoadingIndicator delay={0} />;
|
||||||
return <LoadingIndicator />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = wallet.getAccount(index);
|
let { amount, decimals, mint, tokenName, tokenSymbol } = balanceInfo;
|
||||||
let {
|
|
||||||
amount,
|
|
||||||
decimals,
|
|
||||||
mint,
|
|
||||||
tokenName,
|
|
||||||
tokenSymbol,
|
|
||||||
initialized,
|
|
||||||
} = balanceInfo;
|
|
||||||
|
|
||||||
if (!initialized && index !== 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -140,7 +126,7 @@ function BalanceListItem({ index }) {
|
||||||
{tokenSymbol ?? abbreviateAddress(mint)}
|
{tokenSymbol ?? abbreviateAddress(mint)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
secondary={account.publicKey.toBase58()}
|
secondary={publicKey.toBase58()}
|
||||||
secondaryTypographyProps={{ className: classes.address }}
|
secondaryTypographyProps={{ className: classes.address }}
|
||||||
/>
|
/>
|
||||||
{open ? <ExpandLess /> : <ExpandMore />}
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
@ -165,7 +151,7 @@ function BalanceListItem({ index }) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="body2" className={classes.address}>
|
<Typography variant="body2" className={classes.address}>
|
||||||
Deposit Address: {account.publicKey.toBase58()}
|
Deposit Address: {publicKey.toBase58()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Token Name: {tokenName ?? 'Unknown'}
|
Token Name: {tokenName ?? 'Unknown'}
|
||||||
|
@ -180,7 +166,10 @@ function BalanceListItem({ index }) {
|
||||||
) : null}
|
) : null}
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<Link
|
<Link
|
||||||
href={`https://explorer.solana.com/account/${account.publicKey.toBase58()}`}
|
href={
|
||||||
|
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
|
||||||
|
urlSuffix
|
||||||
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
|
@ -193,7 +182,6 @@ function BalanceListItem({ index }) {
|
||||||
open={sendDialogOpen}
|
open={sendDialogOpen}
|
||||||
onClose={() => setSendDialogOpen(false)}
|
onClose={() => setSendDialogOpen(false)}
|
||||||
balanceInfo={balanceInfo}
|
balanceInfo={balanceInfo}
|
||||||
index={index}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,11 +14,15 @@ const useStyles = makeStyles((theme) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function LoadingIndicator({ height = null, ...rest }) {
|
export default function LoadingIndicator({
|
||||||
|
height = null,
|
||||||
|
delay = 500,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
useEffectAfterTimeout(() => setVisible(true), 500);
|
useEffectAfterTimeout(() => setVisible(true), delay);
|
||||||
|
|
||||||
let style = {};
|
let style = {};
|
||||||
if (height) {
|
if (height) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { abbreviateAddress } from '../utils/utils';
|
||||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||||
import { useSendTransaction } from '../utils/notifications';
|
import { useSendTransaction } from '../utils/notifications';
|
||||||
|
|
||||||
export default function SendDialog({ open, onClose, index, balanceInfo }) {
|
export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
const [destinationAddress, setDestinationAddress] = useState('');
|
const [destinationAddress, setDestinationAddress] = useState('');
|
||||||
const [transferAmountString, setTransferAmountString] = useState('');
|
const [transferAmountString, setTransferAmountString] = useState('');
|
||||||
|
@ -33,7 +33,11 @@ export default function SendDialog({ open, onClose, index, balanceInfo }) {
|
||||||
throw new Error('Invalid amount');
|
throw new Error('Invalid amount');
|
||||||
}
|
}
|
||||||
sendTransaction(
|
sendTransaction(
|
||||||
wallet.transferToken(index, new PublicKey(destinationAddress), amount),
|
wallet.transferToken(
|
||||||
|
publicKey,
|
||||||
|
new PublicKey(destinationAddress),
|
||||||
|
amount,
|
||||||
|
),
|
||||||
{ onSuccess: onClose },
|
{ onSuccess: onClose },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useContext, useEffect, useMemo } from 'react';
|
import React, { useContext, useEffect, useMemo } from 'react';
|
||||||
import { clusterApiUrl, Connection } from '@solana/web3.js';
|
import { clusterApiUrl, Connection } from '@solana/web3.js';
|
||||||
import { useLocalStorageState } from './utils';
|
import { useLocalStorageState } from './utils';
|
||||||
import { refreshCache, useAsyncData } from './fetch-loop';
|
import { refreshCache, setCache, useAsyncData } from './fetch-loop';
|
||||||
import tuple from 'immutable-tuple';
|
import tuple from 'immutable-tuple';
|
||||||
|
|
||||||
const ConnectionContext = React.createContext(null);
|
const ConnectionContext = React.createContext(null);
|
||||||
|
@ -37,6 +37,16 @@ export function useIsProdNetwork() {
|
||||||
return endpoint === clusterApiUrl('mainnet-beta');
|
return endpoint === clusterApiUrl('mainnet-beta');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSolanaExplorerUrlSuffix() {
|
||||||
|
const endpoint = useContext(ConnectionContext).endpoint;
|
||||||
|
if (endpoint === clusterApiUrl('devnet')) {
|
||||||
|
return '?cluster=devnet';
|
||||||
|
} else if (endpoint === clusterApiUrl('testnet')) {
|
||||||
|
return '?cluster=testnet';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export function useAccountInfo(publicKey) {
|
export function useAccountInfo(publicKey) {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const cacheKey = tuple(connection, publicKey?.toBase58());
|
const cacheKey = tuple(connection, publicKey?.toBase58());
|
||||||
|
@ -61,3 +71,8 @@ export function refreshAccountInfo(connection, publicKey, clearCache = false) {
|
||||||
const cacheKey = tuple(connection, publicKey.toBase58());
|
const cacheKey = tuple(connection, publicKey.toBase58());
|
||||||
refreshCache(cacheKey, clearCache);
|
refreshCache(cacheKey, clearCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setInitialAccountInfo(connection, publicKey, accountInfo) {
|
||||||
|
const cacheKey = tuple(connection, publicKey.toBase58());
|
||||||
|
setCache(cacheKey, accountInfo, { initializeOnly: true });
|
||||||
|
}
|
||||||
|
|
|
@ -180,3 +180,14 @@ export function refreshCache(cacheKey, clearCache = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCache(cacheKey, value, { initializeOnly = false } = {}) {
|
||||||
|
if (!initializeOnly && globalCache.has(cacheKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalCache.set(cacheKey, value);
|
||||||
|
const loop = globalLoops.loops.get(cacheKey);
|
||||||
|
if (loop) {
|
||||||
|
loop.notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
import { useConnection } from './connection';
|
import { useConnection, useSolanaExplorerUrlSuffix } from './connection';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
|
|
||||||
export function useSendTransaction() {
|
export function useSendTransaction() {
|
||||||
|
@ -8,7 +8,10 @@ export function useSendTransaction() {
|
||||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
|
||||||
async function sendTransaction(signaturePromise, { onSuccess, onError }) {
|
async function sendTransaction(
|
||||||
|
signaturePromise,
|
||||||
|
{ onSuccess, onError } = {},
|
||||||
|
) {
|
||||||
let id = enqueueSnackbar('Sending transaction...', {
|
let id = enqueueSnackbar('Sending transaction...', {
|
||||||
variant: 'info',
|
variant: 'info',
|
||||||
persist: true,
|
persist: true,
|
||||||
|
@ -36,6 +39,7 @@ export function useSendTransaction() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
closeSnackbar(id);
|
closeSnackbar(id);
|
||||||
setSending(false);
|
setSending(false);
|
||||||
|
console.warn(e.message);
|
||||||
enqueueSnackbar(e.message, { variant: 'error' });
|
enqueueSnackbar(e.message, { variant: 'error' });
|
||||||
if (onError) {
|
if (onError) {
|
||||||
onError(e);
|
onError(e);
|
||||||
|
@ -47,13 +51,14 @@ export function useSendTransaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewTransactionOnExplorerButton({ signature }) {
|
function ViewTransactionOnExplorerButton({ signature }) {
|
||||||
|
const urlSuffix = useSolanaExplorerUrlSuffix();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
component="a"
|
component="a"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
href={`https://explorer.solana.com/tx/${signature}`}
|
href={`https://explorer.solana.com/tx/${signature}` + urlSuffix}
|
||||||
>
|
>
|
||||||
View on Solana Explorer
|
View on Solana Explorer
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -69,7 +74,7 @@ export function useCallAsync() {
|
||||||
successMessage = 'Success',
|
successMessage = 'Success',
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
},
|
} = {},
|
||||||
) {
|
) {
|
||||||
let id = enqueueSnackbar(progressMessage, {
|
let id = enqueueSnackbar(progressMessage, {
|
||||||
variant: 'info',
|
variant: 'info',
|
||||||
|
|
|
@ -27,3 +27,17 @@ export function parseMintData(data) {
|
||||||
let { decimals } = MINT_LAYOUT.decode(data);
|
let { decimals } = MINT_LAYOUT.decode(data);
|
||||||
return { decimals };
|
return { decimals };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOwnedAccountsFilters(publicKey) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: ACCOUNT_LAYOUT.offsetOf('owner'),
|
||||||
|
bytes: publicKey.toBase58(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataSize: ACCOUNT_LAYOUT.span,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,58 @@
|
||||||
import { SystemProgram, Transaction } from '@solana/web3.js';
|
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
||||||
import {
|
import {
|
||||||
initializeAccount,
|
initializeAccount,
|
||||||
initializeMint,
|
initializeMint,
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
transfer,
|
transfer,
|
||||||
} from './instructions';
|
} from './instructions';
|
||||||
import { ACCOUNT_LAYOUT, MINT_LAYOUT } from './data';
|
import { ACCOUNT_LAYOUT, getOwnedAccountsFilters, MINT_LAYOUT } from './data';
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
|
||||||
|
export async function getOwnedTokenAccounts(connection, publicKey) {
|
||||||
|
let filters = getOwnedAccountsFilters(publicKey);
|
||||||
|
let resp = await connection._rpcRequest('getProgramAccounts', [
|
||||||
|
TOKEN_PROGRAM_ID.toBase58(),
|
||||||
|
{
|
||||||
|
commitment: connection.commitment,
|
||||||
|
filters,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
if (resp.error) {
|
||||||
|
throw new Error(
|
||||||
|
'failed to get token accounts owned by ' +
|
||||||
|
publicKey.toBase58() +
|
||||||
|
': ' +
|
||||||
|
resp.error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resp.result
|
||||||
|
.map(({ pubkey, account: { data, executable, owner, lamports } }) => ({
|
||||||
|
publicKey: new PublicKey(pubkey),
|
||||||
|
accountInfo: {
|
||||||
|
data: bs58.decode(data),
|
||||||
|
executable,
|
||||||
|
owner: new PublicKey(owner),
|
||||||
|
lamports,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.filter(({ accountInfo }) => {
|
||||||
|
// TODO: remove this check once mainnet is updated
|
||||||
|
return filters.every((filter) => {
|
||||||
|
if (filter.dataSize) {
|
||||||
|
return accountInfo.data.length === filter.dataSize;
|
||||||
|
} else if (filter.memcmp) {
|
||||||
|
let filterBytes = bs58.decode(filter.memcmp.bytes);
|
||||||
|
return accountInfo.data
|
||||||
|
.slice(
|
||||||
|
filter.memcmp.offset,
|
||||||
|
filter.memcmp.offset + filterBytes.length,
|
||||||
|
)
|
||||||
|
.equals(filterBytes);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function createAndInitializeMint({
|
export async function createAndInitializeMint({
|
||||||
connection,
|
connection,
|
||||||
|
|
|
@ -8,6 +8,7 @@ const customTokenNamesByNetwork = JSON.parse(
|
||||||
);
|
);
|
||||||
|
|
||||||
const nameUpdated = new EventEmitter();
|
const nameUpdated = new EventEmitter();
|
||||||
|
nameUpdated.setMaxListeners(100);
|
||||||
|
|
||||||
export function useTokenName(mint) {
|
export function useTokenName(mint) {
|
||||||
const { endpoint } = useConnectionConfig();
|
const { endpoint } = useConnectionConfig();
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import React, { useContext, useEffect, useMemo } from 'react';
|
import React, { useContext, useMemo } from 'react';
|
||||||
import * as bip32 from 'bip32';
|
import * as bip32 from 'bip32';
|
||||||
import { Account } from '@solana/web3.js';
|
import { Account } from '@solana/web3.js';
|
||||||
import nacl from 'tweetnacl';
|
import nacl from 'tweetnacl';
|
||||||
import {
|
import {
|
||||||
refreshAccountInfo,
|
setInitialAccountInfo,
|
||||||
useAccountInfo,
|
useAccountInfo,
|
||||||
useConnection,
|
useConnection,
|
||||||
} from './connection';
|
} from './connection';
|
||||||
import { createAndInitializeTokenAccount, transferTokens } from './tokens';
|
import {
|
||||||
|
createAndInitializeTokenAccount,
|
||||||
|
getOwnedTokenAccounts,
|
||||||
|
transferTokens,
|
||||||
|
} from './tokens';
|
||||||
import { TOKEN_PROGRAM_ID } from './tokens/instructions';
|
import { TOKEN_PROGRAM_ID } from './tokens/instructions';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_LAYOUT,
|
ACCOUNT_LAYOUT,
|
||||||
parseMintData,
|
parseMintData,
|
||||||
parseTokenAccountData,
|
parseTokenAccountData,
|
||||||
} from './tokens/data';
|
} from './tokens/data';
|
||||||
import EventEmitter from 'events';
|
import { useLocalStorageState } from './utils';
|
||||||
import { useListener, useLocalStorageState } from './utils';
|
|
||||||
import { useTokenName } from './tokens/names';
|
import { useTokenName } from './tokens/names';
|
||||||
|
import { refreshCache, useAsyncData } from './fetch-loop';
|
||||||
|
|
||||||
export class Wallet {
|
export class Wallet {
|
||||||
constructor(connection, seed, walletIndex = 0) {
|
constructor(connection, seed, walletIndex = 0) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
this.walletIndex = walletIndex;
|
this.walletIndex = walletIndex;
|
||||||
this.accountCount = 1;
|
this.account = Wallet.getAccountFromSeed(this.seed, this.walletIndex);
|
||||||
this.account = this.getAccount(0);
|
|
||||||
|
|
||||||
this.emitter = new EventEmitter();
|
|
||||||
this.emitter.setMaxListeners(50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
|
static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
|
||||||
|
@ -37,21 +37,24 @@ export class Wallet {
|
||||||
return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey);
|
return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccount = (index) => {
|
getTokenPublicKeys = async () => {
|
||||||
return Wallet.getAccountFromSeed(this.seed, this.walletIndex, index);
|
let accounts = await getOwnedTokenAccounts(
|
||||||
|
this.connection,
|
||||||
|
this.account.publicKey,
|
||||||
|
);
|
||||||
|
return accounts.map(({ publicKey, accountInfo }) => {
|
||||||
|
setInitialAccountInfo(this.connection, publicKey, accountInfo);
|
||||||
|
return publicKey;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createTokenAccount = async (tokenAddress) => {
|
createTokenAccount = async (tokenAddress) => {
|
||||||
let index = this.accountCount;
|
return await createAndInitializeTokenAccount({
|
||||||
await createAndInitializeTokenAccount({
|
|
||||||
connection: this.connection,
|
connection: this.connection,
|
||||||
payer: this.account,
|
payer: this.account,
|
||||||
mintPublicKey: tokenAddress,
|
mintPublicKey: tokenAddress,
|
||||||
newAccount: this.getAccount(index),
|
newAccount: new Account(),
|
||||||
});
|
});
|
||||||
++this.accountCount;
|
|
||||||
refreshAccountInfo(this.connection, this.getAccount(index).publicKey, true);
|
|
||||||
this.emitter.emit('accountCountChange');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tokenAccountCost = async () => {
|
tokenAccountCost = async () => {
|
||||||
|
@ -60,12 +63,11 @@ export class Wallet {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
transferToken = async (index, destination, amount) => {
|
transferToken = async (source, destination, amount) => {
|
||||||
let tokenAccount = this.getAccount(index);
|
|
||||||
return await transferTokens({
|
return await transferTokens({
|
||||||
connection: this.connection,
|
connection: this.connection,
|
||||||
owner: this.account,
|
owner: this.account,
|
||||||
sourcePublicKey: tokenAccount.publicKey,
|
sourcePublicKey: source,
|
||||||
destinationPublicKey: destination,
|
destinationPublicKey: destination,
|
||||||
amount,
|
amount,
|
||||||
});
|
});
|
||||||
|
@ -103,15 +105,21 @@ export function useWallet() {
|
||||||
return useContext(WalletContext).wallet;
|
return useContext(WalletContext).wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWalletAccountCount() {
|
export function useWalletPublicKeys() {
|
||||||
let wallet = useWallet();
|
let wallet = useWallet();
|
||||||
useListener(wallet.emitter, 'accountCountChange');
|
let [tokenPublicKeys, loaded] = useAsyncData(
|
||||||
return wallet.accountCount;
|
wallet.getTokenPublicKeys,
|
||||||
|
wallet.getTokenPublicKeys,
|
||||||
|
);
|
||||||
|
let publicKeys = [wallet.account.publicKey, ...(tokenPublicKeys ?? [])];
|
||||||
|
return [publicKeys, loaded];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBalanceInfo(index) {
|
export function refreshWalletPublicKeys(wallet) {
|
||||||
let wallet = useWallet();
|
refreshCache(wallet.getTokenPublicKeys);
|
||||||
let publicKey = wallet.getAccount(index).publicKey;
|
}
|
||||||
|
|
||||||
|
export function useBalanceInfo(publicKey) {
|
||||||
let [accountInfo, accountInfoLoaded] = useAccountInfo(publicKey);
|
let [accountInfo, accountInfoLoaded] = useAccountInfo(publicKey);
|
||||||
let { mint, owner, amount } = accountInfo?.owner.equals(TOKEN_PROGRAM_ID)
|
let { mint, owner, amount } = accountInfo?.owner.equals(TOKEN_PROGRAM_ID)
|
||||||
? parseTokenAccountData(accountInfo.data)
|
? parseTokenAccountData(accountInfo.data)
|
||||||
|
@ -119,13 +127,6 @@ export function useBalanceInfo(index) {
|
||||||
let [mintInfo, mintInfoLoaded] = useAccountInfo(mint);
|
let [mintInfo, mintInfoLoaded] = useAccountInfo(mint);
|
||||||
let { name, symbol } = useTokenName(mint);
|
let { name, symbol } = useTokenName(mint);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (accountInfo && wallet.accountCount < index + 1) {
|
|
||||||
wallet.accountCount = index + 1;
|
|
||||||
wallet.emitter.emit('accountCountChange');
|
|
||||||
}
|
|
||||||
}, [wallet, accountInfo, index]);
|
|
||||||
|
|
||||||
if (accountInfoLoaded && mint && mintInfoLoaded) {
|
if (accountInfoLoaded && mint && mintInfoLoaded) {
|
||||||
let { decimals } = parseMintData(mintInfo.data);
|
let { decimals } = parseMintData(mintInfo.data);
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue