Skip to content

Commit

Permalink
close spam token accounts instructions (#1877)
Browse files Browse the repository at this point in the history
  • Loading branch information
abrzezinski94 authored Oct 16, 2023
1 parent b6ef959 commit a94b7cf
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 0 deletions.
5 changes: 5 additions & 0 deletions hooks/useGovernanceAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
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, TransactionInstruction } from '@solana/web3.js'
import { getATA } from '@utils/ataTools'
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 } from '@tools/sdk/units'

interface CloseMultiTokenAccountForm {
wallet: AssetAccount | undefined | null
tokenAccounts: AssetAccount[]
fundsDestinationAccount: string
solRentDestination: string
}

const CloseMultipleTokenAccounts = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const realm = useRealmQuery().data?.result
const wallet = useWalletOnePointOh()
const connection = useLegacyConnectionContext()
const shouldBeGoverned = !!(index !== 0 && governance)
const { governedTokenAccountsWithoutNfts } = useGovernanceAssets()
const [form, setForm] = useState<CloseMultiTokenAccountForm>({
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<UiInstruction> {
const isValid = await validateInstruction({ schema, form, setFormErrors })

const additionalSerializedInstructions: string[] = []
const prerequisiteInstructions: TransactionInstruction[] = []
const mintsOfCurrentlyPushedAtaInstructions: string[] = []
if (
isValid &&
form!.wallet?.governance?.account &&
wallet?.publicKey &&
realm
) {
for (const tokenAccount of form.tokenAccounts) {
const sourceAccount = tokenAccount.pubkey
//this is the original owner
const destinationAccount = new PublicKey(form!.fundsDestinationAccount)
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 {
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 &&
!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
mintPK, // mint
receiverAddress, // ata
destinationAccount, // owner of token account
wallet!.publicKey! // fee payer
)
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 closeInstruction = Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
tokenAccount.pubkey,
new PublicKey(form!.solRentDestination),
tokenAccount.extensions.token!.account.owner,
[]
)
additionalSerializedInstructions.push(
serializeInstructionToBase64(closeInstruction)
)
}
}
const obj: UiInstruction = {
prerequisiteInstructions: prerequisiteInstructions,
serializedInstruction: '',
additionalSerializedInstructions: additionalSerializedInstructions,
chunkBy: 3,
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: (
<div>
{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 pubkey = x.pubkey.toBase58()
const tokenName = info?.name ? info.name : ''
const amount = fmtMintAmount(
x.extensions.mint!.account,
x!.extensions.token!.account.amount
)
return (
<div className="mb-4" key={x.pubkey.toBase58()}>
<Checkbox
label={`${pubkey} ${amount} ${tokenName}`}
checked={
!!form.tokenAccounts?.find(
(toAcc) =>
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,
})
}}
></Checkbox>
</div>
)
})}
</div>
),
},
{
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 (
<>
<InstructionForm
outerForm={form}
setForm={setForm}
inputs={inputs}
setFormErrors={setFormErrors}
formErrors={formErrors}
></InstructionForm>
</>
)
}

export default CloseMultipleTokenAccounts
2 changes: 2 additions & 0 deletions pages/dao/[symbol]/proposal/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions utils/uiTypes/proposalCreationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export enum Instructions {
ChangeMakeDonation,
Clawback,
CloseTokenAccount,
CloseMultipleTokenAccounts,
ConfigureGatewayPlugin,
ConfigureNftPluginCollection,
CreateAssociatedTokenAccount,
Expand Down

1 comment on commit a94b7cf

@vercel
Copy link

@vercel vercel bot commented on a94b7cf Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

governance-ui – ./

governance-ui-git-main-solana-labs.vercel.app
governance-ui-solana-labs.vercel.app
app.realms.today

Please sign in to comment.