Skip to content

Instantly share code, notes, and snippets.

@NotoriousPyro
Last active May 21, 2024 22:34
Show Gist options
  • Save NotoriousPyro/0082720b892ca8af0d9f1580e0118c83 to your computer and use it in GitHub Desktop.
Save NotoriousPyro/0082720b892ca8af0d9f1580e0118c83 to your computer and use it in GitHub Desktop.
Burn your unwanted ATAs on Solana with this script
import { GetProgramAccountsFilter, PublicKey, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
import { connection } from "../src/connection";
import config from "../src/lib/Config";
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createBurnCheckedInstruction, createCloseAccountInstruction, getAssociatedTokenAddressSync, unpackAccount, unpackMint } from "@solana/spl-token";
// Not really tested more than two but you can probably fit more in.
const maxMintsPerTx = 2;
// Replace this with your own keypair
const owningAccount = config.keypair;
// This creates a tx like this: https://solscan.io/tx/5PegaAnFzaEdVWYfzHFVTzJyZr8wHKRhijgtXyGhNzaQj4HfW5U8BqaaHNhGkCbZRDePns5cnFJAb18RCx4cuLqB
const mints = [
"9UVjMeTLsXq4Nb9CdpaWgJ4D79Vpmcpx6Bks1vZ9UU6P",
"5qY9aMeoqLXdv6CCfGnURz7WUjvKK3UQdAMsEcAXPTos"
]
const createComputeBudgetInstruction = (computeUnitLimit: number = 600000, computeUnitPrice: number = 1) => [
ComputeBudgetProgram.setComputeUnitLimit({
units: computeUnitLimit // compute units
}),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: computeUnitPrice // priority fee
})
];
export async function* generateAccountInfos(
publicKeys: PublicKey[],
chunkSize: number = 100,
commitment: Commitment = "confirmed"
): AsyncGenerator<{ publicKey: PublicKey, accountInfo: AccountInfo<Buffer>, slot: number }> {
for (let i = 0; i < publicKeys.length; i += chunkSize) {
const chunk = publicKeys.slice(i, i + chunkSize);
const accountInfos = await connection.getMultipleAccountsInfoAndContext(chunk, { commitment });
for (const [index, accountInfo] of accountInfos.value.entries()) {
yield { publicKey: chunk[index], accountInfo, slot: accountInfos.context.slot };
}
}
}
export const getAccountInfos = async (
publicKeys: PublicKey[],
chunkSize: number = 100,
commitment: Commitment = "confirmed"
): Promise<{ publicKey: PublicKey, accountInfo: AccountInfo<Buffer>, slot: number }[]> => {
const accountInfos = [];
for await (const accountInfo of generateAccountInfos(publicKeys, chunkSize, commitment)) {
accountInfos.push(accountInfo);
}
return accountInfos;
}
/**
* Finds the ATAs for the given mints and burns them, closes the account and returns the tokens to the owner.
*/
const burnTokens = async (mints: string[]) => {
const allTokenAccountsFilter: GetProgramAccountsFilter[] = [
{
dataSize: 165
}, {
memcmp: {
offset: 32,
bytes: owningAccount.toString(),
}
}
];
const allTokenAccounts = await connection.getProgramAccounts(
TOKEN_PROGRAM_ID, { filters: allTokenAccountsFilter }
);
const allToken2022Accounts = await connection.getProgramAccounts(
TOKEN_2022_PROGRAM_ID, { filters: allTokenAccountsFilter }
);
const unpackedATAs = allTokenAccounts.map(
tokenAccount => unpackAccount(tokenAccount.pubkey, tokenAccount.account)
);
const unpacked2022ATAs = allToken2022Accounts.map(
tokenAccount => unpackAccount(tokenAccount.pubkey, tokenAccount.account)
);
const mintAccountInfo = await getAccountInfos(mints.map(mint => new PublicKey(mint)));
const closeInstructions = mintAccountInfo.map(mint => {
const ata = getAssociatedTokenAddressSync(mint.publicKey, owningAccount.publicKey, false, mint.accountInfo.owner);
const ataInfo = mint.accountInfo.owner.equals(TOKEN_2022_PROGRAM_ID)
? unpacked2022ATAs.find(pa => pa.address.equals(ata))
: unpackedATAs.find(pa => pa.address.equals(ata))
;
const unpackedMint = unpackMint(mint.publicKey, mint.accountInfo)
const burn = createBurnCheckedInstruction(
ata,
mint.publicKey,
owningAccount.publicKey,
ataInfo.amount,
unpackedMint.decimals,
undefined,
mint.accountInfo.owner
)
const close = createCloseAccountInstruction(
ata,
owningAccount.publicKey,
owningAccount.publicKey,
undefined,
mint.accountInfo.owner
);
return [burn, close];
}).filter(instruction => instruction).flat();
const bhInfo = await connection.getLatestBlockhashAndContext({ commitment: "confirmed" });
const messageV0 = new TransactionMessage({
payerKey: owningAccount.publicKey,
recentBlockhash: bhInfo.value.blockhash,
instructions: [
...createComputeBudgetInstruction(
100_000, 100
),
...closeInstructions,
],
}).compileToV0Message();
const tx = new VersionedTransaction(messageV0);
tx.sign([owningAccount]);
const simulation = await connection.simulateTransaction(tx, { commitment: "confirmed" });
if (simulation.value.err) {
throw simulation.value.err;
}
console.log("Simulation: ", simulation.value);
try {
const signature = await connection.sendTransaction(tx, {
maxRetries: 20,
skipPreflight: true,
});
const confirmation = await connection.confirmTransaction({
signature,
blockhash: bhInfo.value.blockhash,
lastValidBlockHeight: bhInfo.value.lastValidBlockHeight,
}, "confirmed");
if (confirmation.value.err) {
throw new Error(`Transaction not confirmed: ${confirmation.value.err.toString()}`);
}
console.log("Confirmed: ", signature);
} catch (error) {
console.error("Failed to burn accounts", error);
throw error;
}
}
const main = async () => {
for (let i = 0; i < mints.length; i += maxMintsPerTx) {
const chunk = mints.slice(i, i + maxMintsPerTx);
await burnTokens(chunk);
}
console.log("Done");
process.exit(0);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment