From 02b4748fb5b90a1a23dcfa312a9771536ededd5b Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Mon, 5 Oct 2020 03:13:24 -0700 Subject: [PATCH] Add a button to delete token accounts --- src/components/BalancesList.js | 25 ++++++++++- src/components/CloseTokenAccountButton.js | 52 +++++++++++++++++++++++ src/utils/connection.js | 14 +++--- src/utils/tokens/index.js | 19 +++++++++ src/utils/tokens/instructions.js | 16 +++++++ src/utils/wallet.js | 9 ++++ 6 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 src/components/CloseTokenAccountButton.js diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index f795bfb..ea410d9 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -21,6 +21,7 @@ import { abbreviateAddress } from '../utils/utils'; import Button from '@material-ui/core/Button'; import SendIcon from '@material-ui/icons/Send'; import ReceiveIcon from '@material-ui/icons/WorkOutline'; +import DeleteIcon from '@material-ui/icons/Delete'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import AddIcon from '@material-ui/icons/Add'; @@ -36,6 +37,7 @@ import { useSolanaExplorerUrlSuffix, } from '../utils/connection'; import { showTokenInfoDialog } from '../utils/config'; +import CloseTokenAccountDialog from './CloseTokenAccountButton'; const balanceFormat = new Intl.NumberFormat(undefined, { minimumFractionDigits: 4, @@ -150,12 +152,16 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) { const [sendDialogOpen, setSendDialogOpen] = useState(false); const [depositDialogOpen, setDepositDialogOpen] = useState(false); const [tokenInfoDialogOpen, setTokenInfoDialogOpen] = useState(false); + const [ + closeTokenAccountDialogOpen, + setCloseTokenAccountDialogOpen, + ] = useState(false); if (!balanceInfo) { return ; } - let { mint, tokenName, tokenSymbol, owner } = balanceInfo; + let { mint, tokenName, tokenSymbol, owner, amount } = balanceInfo; return ( <> @@ -187,6 +193,17 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) { > Send + {mint && amount === 0 ? ( + + ) : null} Deposit Address: {publicKey.toBase58()} @@ -233,6 +250,12 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) { balanceInfo={balanceInfo} publicKey={publicKey} /> + setCloseTokenAccountDialogOpen(false)} + balanceInfo={balanceInfo} + publicKey={publicKey} + /> ); } diff --git a/src/components/CloseTokenAccountButton.js b/src/components/CloseTokenAccountButton.js new file mode 100644 index 0000000..33852c3 --- /dev/null +++ b/src/components/CloseTokenAccountButton.js @@ -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 ( + + + Delete {tokenName ?? mint.toBase58()} Address{' '} + {abbreviateAddress(publicKey)} + + + + 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. + + + + + + + + ); +} diff --git a/src/utils/connection.js b/src/utils/connection.js index 4c32255..06b3ef8 100644 --- a/src/utils/connection.js +++ b/src/utils/connection.js @@ -59,13 +59,15 @@ export function useAccountInfo(publicKey) { if (!publicKey) { return; } - let previousData = null; + let previousInfo = null; const id = connection.onAccountChange(publicKey, (info) => { - if (info.data) { - if (!previousData || !previousData.equals(info.data)) { - previousData = info.data; - setCache(cacheKey, info); - } + if ( + !previousInfo || + !previousInfo.data.equals(info.data) || + previousInfo.lamports !== info.lamports + ) { + previousInfo = info; + setCache(cacheKey, info); } }); return () => connection.removeAccountChangeListener(id); diff --git a/src/utils/tokens/index.js b/src/utils/tokens/index.js index 898d1da..0571439 100644 --- a/src/utils/tokens/index.js +++ b/src/utils/tokens/index.js @@ -1,5 +1,6 @@ import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; import { + closeAccount, initializeAccount, initializeMint, memoInstruction, @@ -173,3 +174,21 @@ export async function transferTokens({ 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', + }); +} diff --git a/src/utils/tokens/instructions.js b/src/utils/tokens/instructions.js index e2c8aae..5916458 100644 --- a/src/utils/tokens/instructions.js +++ b/src/utils/tokens/instructions.js @@ -44,6 +44,7 @@ LAYOUT.addVariant( BufferLayout.struct([BufferLayout.nu64('amount')]), 'burn', ); +LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount'); const instructionMaxSpan = Math.max( ...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) { return new TransactionInstruction({ keys: [], diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 4f50e1c..0d1e2e1 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -8,6 +8,7 @@ import { useConnection, } from './connection'; import { + closeTokenAccount, createAndInitializeTokenAccount, getOwnedTokenAccounts, transferTokens, @@ -97,6 +98,14 @@ export class Wallet { preflightCommitment: 'single', }); }; + + closeTokenAccount = async (publicKey) => { + return await closeTokenAccount({ + connection: this.connection, + owner: this.account, + sourcePublicKey: publicKey, + }); + }; } const WalletContext = React.createContext(null);