From 60b13a298242474f290dd1b803724246899ca559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezin=CC=81ski?= Date: Mon, 16 Oct 2023 19:03:24 +0200 Subject: [PATCH 1/2] close spam wallets wip --- hooks/useGovernanceAssets.ts | 5 + .../CloseMultipleTokenAccounts.tsx | 256 ++++++++++++++++++ pages/dao/[symbol]/proposal/new.tsx | 2 + utils/uiTypes/proposalCreationTypes.ts | 1 + 4 files changed, 264 insertions(+) create mode 100644 pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index dc3e80dcb5..66646cd16a 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -243,6 +243,11 @@ export default function useGovernanceAssets() { isVisible: canUseTransferInstruction, packageId: PackageEnum.Common, }, + [Instructions.CloseMultipleTokenAccounts]: { + name: 'Close multiple token accounts', + isVisible: canUseTransferInstruction, + packageId: PackageEnum.Common, + }, [Instructions.CreateAssociatedTokenAccount]: { name: 'Create Associated Token Account', packageId: PackageEnum.Common, diff --git a/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx b/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx new file mode 100644 index 0000000000..b50e1e04b6 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx @@ -0,0 +1,256 @@ +import { useContext, useEffect, useState } from 'react' +import { + Governance, + ProgramAccount, + serializeInstructionToBase64, +} from '@solana/spl-governance' +import { validateInstruction } from '@utils/instructionTools' +import { UiInstruction } from '@utils/uiTypes/proposalCreationTypes' + +import { NewProposalContext } from '../../new' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import { AssetAccount } from '@utils/uiTypes/assets' +import InstructionForm, { InstructionInput } from './FormCreator' +import { InstructionInputType } from './inputInstructionType' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import * as yup from 'yup' +import { PublicKey } from '@solana/web3.js' +import { getATA } from '@utils/ataTools' +import { sendTransactionsV3, SequenceType } from '@utils/sendTransactions' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useRealmQuery } from '@hooks/queries/realm' +import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import Checkbox from '@components/inputs/Checkbox' +import tokenPriceService from '@utils/services/tokenPrice' +import { + fmtMintAmount, + formatMintNaturalAmountAsDecimal, +} from '@tools/sdk/units' + +interface CloseMultiTokenAccountForm { + wallet: AssetAccount | undefined | null + tokenAccounts: AssetAccount[] + fundsDestinationAccount: string + solRentDestination: string +} + +const CloseMultipleTokenAccounts = ({ + index, + governance, +}: { + index: number + governance: ProgramAccount | null +}) => { + const realm = useRealmQuery().data?.result + const wallet = useWalletOnePointOh() + const connection = useLegacyConnectionContext() + const shouldBeGoverned = !!(index !== 0 && governance) + const { governedTokenAccountsWithoutNfts } = useGovernanceAssets() + const [form, setForm] = useState({ + wallet: null, + tokenAccounts: [], + fundsDestinationAccount: '', + solRentDestination: '', + }) + const [formErrors, setFormErrors] = useState({}) + const { handleSetInstructions } = useContext(NewProposalContext) + const schema = yup.object().shape({ + wallet: yup + .object() + .nullable() + .required('Program governed account is required'), + }) + async function getInstruction(): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }) + let serializedInstructionClose = '' + const additionalSerializedInstructions: string[] = [] + if ( + isValid && + form!.wallet?.governance?.account && + wallet?.publicKey && + realm + ) { + if (!form!.wallet.extensions.token!.account.amount?.isZero()) { + const sourceAccount = form!.wallet.extensions.token?.publicKey + //this is the original owner + const destinationAccount = new PublicKey(form!.fundsDestinationAccount) + const mintPK = form!.wallet.extensions.mint!.publicKey + const amount = form!.wallet.extensions.token!.account.amount + + //we find true receiver address if its wallet and we need to create ATA the ata address will be the receiver + const { + currentAddress: receiverAddress, + needToCreateAta, + } = await getATA({ + connection: connection, + receiverAddress: destinationAccount, + mintPK, + wallet: wallet!, + }) + //we push this createATA instruction to transactions to create right before creating proposal + //we don't want to create ata only when instruction is serialized + if (needToCreateAta) { + const createAtaInstruction = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID + TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID + mintPK, // mint + receiverAddress, // ata + destinationAccount, // owner of token account + wallet!.publicKey! // fee payer + ) + //ata needs to be created before otherwise simulations will throw errors. + //createCloseAccountInstruction has check if ata is existing its not like in transfer where we can run + //simulation without created ata and we create it on the fly before proposal + await sendTransactionsV3({ + connection: connection.current, + wallet: wallet, + transactionInstructions: [ + { + instructionsSet: [ + { + transactionInstruction: createAtaInstruction, + }, + ], + sequenceType: SequenceType.Parallel, + }, + ], + }) + } + const transferIx = Token.createTransferInstruction( + TOKEN_PROGRAM_ID, + sourceAccount!, + receiverAddress, + form!.wallet!.extensions!.token!.account.owner, + [], + amount + ) + additionalSerializedInstructions.push( + serializeInstructionToBase64(transferIx) + ) + } + + const closeInstruction = Token.createCloseAccountInstruction( + TOKEN_PROGRAM_ID, + form!.wallet.extensions.token!.publicKey!, + new PublicKey(form!.solRentDestination), + form!.wallet.extensions.token!.account.owner!, + [] + ) + serializedInstructionClose = serializeInstructionToBase64( + closeInstruction + ) + additionalSerializedInstructions.push(serializedInstructionClose) + } + const obj: UiInstruction = { + prerequisiteInstructions: [], + serializedInstruction: '', + additionalSerializedInstructions: additionalSerializedInstructions, + isValid, + governance: form!.wallet?.governance, + } + + return obj + } + useEffect(() => { + handleSetInstructions( + { governedAccount: form?.wallet?.governance, getInstruction }, + index + ) + // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree + }, [form]) + const inputs: InstructionInput[] = [ + { + label: 'Wallet', + initialValue: null, + name: 'wallet', + type: InstructionInputType.GOVERNED_ACCOUNT, + shouldBeGoverned: shouldBeGoverned as any, + governance: governance, + options: governedTokenAccountsWithoutNfts.filter((x) => x.isSol), + assetType: 'wallet', + additionalComponent: ( +
+ {governedTokenAccountsWithoutNfts + .filter( + (x) => + x.isToken && + (x.extensions.token?.account.owner.toBase58() === + form.wallet?.extensions.transferAddress?.toBase58() || + x.extensions.token?.account.owner.toBase58() === + form.wallet?.governance.pubkey.toBase58()) + ) + .sort((a, b) => { + const AAmount = fmtMintAmount( + a.extensions.mint!.account, + a!.extensions.token!.account.amount + ) + + const BAmount = fmtMintAmount( + b.extensions.mint!.account, + b!.extensions.token!.account.amount + ) + + return BAmount.length - AAmount.length + }) + .map((x) => { + const info = tokenPriceService.getTokenInfo( + x.extensions.mint!.publicKey.toBase58() + ) + const imgUrl = info?.logoURI ? info.logoURI : '' + const pubkey = x.pubkey.toBase58() + const tokenName = info?.name ? info.name : '' + const amount = fmtMintAmount( + x.extensions.mint!.account, + x!.extensions.token!.account.amount + ) + console.log() + return ( +
+ +
+ ) + })} +
+ ), + }, + { + label: 'Token recipient', + initialValue: '', + name: 'fundsDestinationAccount', + type: InstructionInputType.INPUT, + inputType: 'text', + hide: form?.wallet?.extensions.amount?.isZero(), + }, + { + label: 'Sol recipient', + initialValue: + governedTokenAccountsWithoutNfts + .find((x) => x.isSol) + ?.extensions.transferAddress?.toBase58() || + wallet?.publicKey?.toBase58(), + name: 'solRentDestination', + type: InstructionInputType.INPUT, + inputType: 'text', + }, + ] + return ( + <> + + + ) +} + +export default CloseMultipleTokenAccounts diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx index 42fd0fbc88..c6709da8ac 100644 --- a/pages/dao/[symbol]/proposal/new.tsx +++ b/pages/dao/[symbol]/proposal/new.tsx @@ -70,6 +70,7 @@ import MakeAddMarketListToCategoryParams from './components/instructions/Foresig import RealmConfig from './components/instructions/RealmConfig' import MakeSetMarketMetadataParams from './components/instructions/Foresight/MakeSetMarketMetadataParams' import CloseTokenAccount from './components/instructions/CloseTokenAccount' +import CloseMultipleTokenAccounts from './components/instructions/CloseMultipleTokenAccounts' import { InstructionDataWithHoldUpTime } from 'actions/createProposal' import StakingOption from './components/instructions/Dual/StakingOption' import MeanCreateAccount from './components/instructions/Mean/MeanCreateAccount' @@ -524,6 +525,7 @@ const New = () => { [Instructions.CreateNftPluginMaxVoterWeight]: CreateNftPluginMaxVoterWeightRecord, [Instructions.ConfigureNftPluginCollection]: ConfigureNftPluginCollection, [Instructions.CloseTokenAccount]: CloseTokenAccount, + [Instructions.CloseMultipleTokenAccounts]: CloseMultipleTokenAccounts, [Instructions.VotingMintConfig]: VotingMintConfig, [Instructions.CreateVsrRegistrar]: CreateVsrRegistrar, [Instructions.CreateGatewayPluginRegistrar]: CreateGatewayPluginRegistrar, diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index b73d59cf25..62cf0600d7 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -317,6 +317,7 @@ export enum Instructions { ChangeMakeDonation, Clawback, CloseTokenAccount, + CloseMultipleTokenAccounts, ConfigureGatewayPlugin, ConfigureNftPluginCollection, CreateAssociatedTokenAccount, From 8682724668aad73a615885c5554a0f5d1b879fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezin=CC=81ski?= Date: Mon, 16 Oct 2023 21:28:48 +0200 Subject: [PATCH 2/2] checkpoint --- .../CloseMultipleTokenAccounts.tsx | 116 ++++++++++-------- 1 file changed, 64 insertions(+), 52 deletions(-) diff --git a/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx b/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx index b50e1e04b6..486541696d 100644 --- a/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx +++ b/pages/dao/[symbol]/proposal/components/instructions/CloseMultipleTokenAccounts.tsx @@ -18,18 +18,14 @@ import { TOKEN_PROGRAM_ID, } from '@solana/spl-token' import * as yup from 'yup' -import { PublicKey } from '@solana/web3.js' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { getATA } from '@utils/ataTools' -import { sendTransactionsV3, SequenceType } from '@utils/sendTransactions' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useRealmQuery } from '@hooks/queries/realm' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import Checkbox from '@components/inputs/Checkbox' import tokenPriceService from '@utils/services/tokenPrice' -import { - fmtMintAmount, - formatMintNaturalAmountAsDecimal, -} from '@tools/sdk/units' +import { fmtMintAmount } from '@tools/sdk/units' interface CloseMultiTokenAccountForm { wallet: AssetAccount | undefined | null @@ -66,20 +62,22 @@ const CloseMultipleTokenAccounts = ({ }) async function getInstruction(): Promise { const isValid = await validateInstruction({ schema, form, setFormErrors }) - let serializedInstructionClose = '' + const additionalSerializedInstructions: string[] = [] + const prerequisiteInstructions: TransactionInstruction[] = [] + const mintsOfCurrentlyPushedAtaInstructions: string[] = [] if ( isValid && form!.wallet?.governance?.account && wallet?.publicKey && realm ) { - if (!form!.wallet.extensions.token!.account.amount?.isZero()) { - const sourceAccount = form!.wallet.extensions.token?.publicKey + for (const tokenAccount of form.tokenAccounts) { + const sourceAccount = tokenAccount.pubkey //this is the original owner const destinationAccount = new PublicKey(form!.fundsDestinationAccount) - const mintPK = form!.wallet.extensions.mint!.publicKey - const amount = form!.wallet.extensions.token!.account.amount + const mintPK = tokenAccount.extensions.mint!.publicKey + const amount = tokenAccount.extensions.token!.account.amount //we find true receiver address if its wallet and we need to create ATA the ata address will be the receiver const { @@ -93,7 +91,12 @@ const CloseMultipleTokenAccounts = ({ }) //we push this createATA instruction to transactions to create right before creating proposal //we don't want to create ata only when instruction is serialized - if (needToCreateAta) { + if ( + needToCreateAta && + !mintsOfCurrentlyPushedAtaInstructions.find( + (x) => x !== mintPK.toBase58() + ) + ) { const createAtaInstruction = Token.createAssociatedTokenAccountInstruction( ASSOCIATED_TOKEN_PROGRAM_ID, // always ASSOCIATED_TOKEN_PROGRAM_ID TOKEN_PROGRAM_ID, // always TOKEN_PROGRAM_ID @@ -102,53 +105,41 @@ const CloseMultipleTokenAccounts = ({ destinationAccount, // owner of token account wallet!.publicKey! // fee payer ) - //ata needs to be created before otherwise simulations will throw errors. - //createCloseAccountInstruction has check if ata is existing its not like in transfer where we can run - //simulation without created ata and we create it on the fly before proposal - await sendTransactionsV3({ - connection: connection.current, - wallet: wallet, - transactionInstructions: [ - { - instructionsSet: [ - { - transactionInstruction: createAtaInstruction, - }, - ], - sequenceType: SequenceType.Parallel, - }, - ], - }) + mintsOfCurrentlyPushedAtaInstructions.push(mintPK.toBase58()) + prerequisiteInstructions.push(createAtaInstruction) + } + + if (!amount.isZero()) { + const transferIx = Token.createTransferInstruction( + TOKEN_PROGRAM_ID, + sourceAccount!, + receiverAddress, + tokenAccount.extensions.token!.account.owner, + [], + amount + ) + additionalSerializedInstructions.push( + serializeInstructionToBase64(transferIx) + ) } - const transferIx = Token.createTransferInstruction( + + const closeInstruction = Token.createCloseAccountInstruction( TOKEN_PROGRAM_ID, - sourceAccount!, - receiverAddress, - form!.wallet!.extensions!.token!.account.owner, - [], - amount + tokenAccount.pubkey, + new PublicKey(form!.solRentDestination), + tokenAccount.extensions.token!.account.owner, + [] ) additionalSerializedInstructions.push( - serializeInstructionToBase64(transferIx) + serializeInstructionToBase64(closeInstruction) ) } - - const closeInstruction = Token.createCloseAccountInstruction( - TOKEN_PROGRAM_ID, - form!.wallet.extensions.token!.publicKey!, - new PublicKey(form!.solRentDestination), - form!.wallet.extensions.token!.account.owner!, - [] - ) - serializedInstructionClose = serializeInstructionToBase64( - closeInstruction - ) - additionalSerializedInstructions.push(serializedInstructionClose) } const obj: UiInstruction = { - prerequisiteInstructions: [], + prerequisiteInstructions: prerequisiteInstructions, serializedInstruction: '', additionalSerializedInstructions: additionalSerializedInstructions, + chunkBy: 3, isValid, governance: form!.wallet?.governance, } @@ -162,6 +153,7 @@ const CloseMultipleTokenAccounts = ({ ) // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree }, [form]) + const inputs: InstructionInput[] = [ { label: 'Wallet', @@ -200,19 +192,39 @@ const CloseMultipleTokenAccounts = ({ const info = tokenPriceService.getTokenInfo( x.extensions.mint!.publicKey.toBase58() ) - const imgUrl = info?.logoURI ? info.logoURI : '' const pubkey = x.pubkey.toBase58() const tokenName = info?.name ? info.name : '' const amount = fmtMintAmount( x.extensions.mint!.account, x!.extensions.token!.account.amount ) - console.log() return (
+ toAcc.pubkey.toBase58() === x.pubkey.toBase58() + ) + } + onChange={(e) => { + let newTokenAccounts = form.tokenAccounts + ? [...form.tokenAccounts] + : [] + if (e.target.checked) { + newTokenAccounts = [...newTokenAccounts, x] + } else { + newTokenAccounts = newTokenAccounts.filter( + (toAcc) => + toAcc.pubkey.toBase58() !== x.pubkey.toBase58() + ) + } + setForm({ + ...form, + tokenAccounts: newTokenAccounts, + }) + }} >
)