Add a button to delete token accounts

This commit is contained in:
Gary Wang 2020-10-05 03:13:24 -07:00
parent ee8018c6fd
commit 02b4748fb5
6 changed files with 128 additions and 7 deletions

View File

@ -21,6 +21,7 @@ import { abbreviateAddress } from '../utils/utils';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import SendIcon from '@material-ui/icons/Send'; import SendIcon from '@material-ui/icons/Send';
import ReceiveIcon from '@material-ui/icons/WorkOutline'; import ReceiveIcon from '@material-ui/icons/WorkOutline';
import DeleteIcon from '@material-ui/icons/Delete';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import AddIcon from '@material-ui/icons/Add'; import AddIcon from '@material-ui/icons/Add';
@ -36,6 +37,7 @@ import {
useSolanaExplorerUrlSuffix, useSolanaExplorerUrlSuffix,
} from '../utils/connection'; } from '../utils/connection';
import { showTokenInfoDialog } from '../utils/config'; import { showTokenInfoDialog } from '../utils/config';
import CloseTokenAccountDialog from './CloseTokenAccountButton';
const balanceFormat = new Intl.NumberFormat(undefined, { const balanceFormat = new Intl.NumberFormat(undefined, {
minimumFractionDigits: 4, minimumFractionDigits: 4,
@ -150,12 +152,16 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
const [sendDialogOpen, setSendDialogOpen] = useState(false); const [sendDialogOpen, setSendDialogOpen] = useState(false);
const [depositDialogOpen, setDepositDialogOpen] = useState(false); const [depositDialogOpen, setDepositDialogOpen] = useState(false);
const [tokenInfoDialogOpen, setTokenInfoDialogOpen] = useState(false); const [tokenInfoDialogOpen, setTokenInfoDialogOpen] = useState(false);
const [
closeTokenAccountDialogOpen,
setCloseTokenAccountDialogOpen,
] = useState(false);
if (!balanceInfo) { if (!balanceInfo) {
return <LoadingIndicator delay={0} />; return <LoadingIndicator delay={0} />;
} }
let { mint, tokenName, tokenSymbol, owner } = balanceInfo; let { mint, tokenName, tokenSymbol, owner, amount } = balanceInfo;
return ( return (
<> <>
@ -187,6 +193,17 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
> >
Send Send
</Button> </Button>
{mint && amount === 0 ? (
<Button
variant="outlined"
color="secondary"
size="small"
startIcon={<DeleteIcon />}
onClick={() => setCloseTokenAccountDialogOpen(true)}
>
Delete
</Button>
) : null}
</div> </div>
<Typography variant="body2" className={classes.address}> <Typography variant="body2" className={classes.address}>
Deposit Address: {publicKey.toBase58()} Deposit Address: {publicKey.toBase58()}
@ -233,6 +250,12 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
balanceInfo={balanceInfo} balanceInfo={balanceInfo}
publicKey={publicKey} publicKey={publicKey}
/> />
<CloseTokenAccountDialog
open={closeTokenAccountDialogOpen}
onClose={() => setCloseTokenAccountDialogOpen(false)}
balanceInfo={balanceInfo}
publicKey={publicKey}
/>
</> </>
); );
} }

View File

@ -0,0 +1,52 @@
import DialogForm from './DialogForm';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import { DialogContentText } from '@material-ui/core';
import { abbreviateAddress } from '../utils/utils';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import React from 'react';
import { useSendTransaction } from '../utils/notifications';
import { refreshWalletPublicKeys, useWallet } from '../utils/wallet';
export default function CloseTokenAccountDialog({
open,
onClose,
publicKey,
balanceInfo,
}) {
const wallet = useWallet();
const [sendTransaction, sending] = useSendTransaction();
const { mint, tokenName } = balanceInfo;
function onSubmit() {
sendTransaction(wallet.closeTokenAccount(publicKey), {
onSuccess: () => {
refreshWalletPublicKeys(wallet);
onClose();
},
});
}
return (
<DialogForm open={open} onClose={onClose} onSubmit={onSubmit}>
<DialogTitle>
Delete {tokenName ?? mint.toBase58()} Address{' '}
{abbreviateAddress(publicKey)}
</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to delete your {tokenName ?? mint.toBase58()}{' '}
address {publicKey.toBase58()}? This will permanently disable token
transfers to this address and remove it from your wallet.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button type="submit" color="secondary" disabled={sending}>
Delete
</Button>
</DialogActions>
</DialogForm>
);
}

View File

@ -59,13 +59,15 @@ export function useAccountInfo(publicKey) {
if (!publicKey) { if (!publicKey) {
return; return;
} }
let previousData = null; let previousInfo = null;
const id = connection.onAccountChange(publicKey, (info) => { const id = connection.onAccountChange(publicKey, (info) => {
if (info.data) { if (
if (!previousData || !previousData.equals(info.data)) { !previousInfo ||
previousData = info.data; !previousInfo.data.equals(info.data) ||
setCache(cacheKey, info); previousInfo.lamports !== info.lamports
} ) {
previousInfo = info;
setCache(cacheKey, info);
} }
}); });
return () => connection.removeAccountChangeListener(id); return () => connection.removeAccountChangeListener(id);

View File

@ -1,5 +1,6 @@
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
import { import {
closeAccount,
initializeAccount, initializeAccount,
initializeMint, initializeMint,
memoInstruction, memoInstruction,
@ -173,3 +174,21 @@ export async function transferTokens({
preflightCommitment: 'single', preflightCommitment: 'single',
}); });
} }
export async function closeTokenAccount({
connection,
owner,
sourcePublicKey,
}) {
let transaction = new Transaction().add(
closeAccount({
source: sourcePublicKey,
destination: owner.publicKey,
owner: owner.publicKey,
}),
);
let signers = [owner];
return await connection.sendTransaction(transaction, signers, {
preflightCommitment: 'single',
});
}

View File

@ -44,6 +44,7 @@ LAYOUT.addVariant(
BufferLayout.struct([BufferLayout.nu64('amount')]), BufferLayout.struct([BufferLayout.nu64('amount')]),
'burn', 'burn',
); );
LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount');
const instructionMaxSpan = Math.max( const instructionMaxSpan = Math.max(
...Object.values(LAYOUT.registry).map((r) => r.span), ...Object.values(LAYOUT.registry).map((r) => r.span),
@ -127,6 +128,21 @@ export function mintTo({ mint, destination, amount, mintAuthority }) {
}); });
} }
export function closeAccount({ source, destination, owner }) {
const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
closeAccount: {},
}),
programId: TOKEN_PROGRAM_ID,
});
}
export function memoInstruction(memo) { export function memoInstruction(memo) {
return new TransactionInstruction({ return new TransactionInstruction({
keys: [], keys: [],

View File

@ -8,6 +8,7 @@ import {
useConnection, useConnection,
} from './connection'; } from './connection';
import { import {
closeTokenAccount,
createAndInitializeTokenAccount, createAndInitializeTokenAccount,
getOwnedTokenAccounts, getOwnedTokenAccounts,
transferTokens, transferTokens,
@ -97,6 +98,14 @@ export class Wallet {
preflightCommitment: 'single', preflightCommitment: 'single',
}); });
}; };
closeTokenAccount = async (publicKey) => {
return await closeTokenAccount({
connection: this.connection,
owner: this.account,
sourcePublicKey: publicKey,
});
};
} }
const WalletContext = React.createContext(null); const WalletContext = React.createContext(null);