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