123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- import { describe, it } from 'node:test';
- import * as anchor from '@coral-xyz/anchor';
- import {
- ASSOCIATED_TOKEN_PROGRAM_ID,
- AccountLayout,
- ExtensionType,
- TOKEN_2022_PROGRAM_ID,
- createAssociatedTokenAccountInstruction,
- createInitializeMintInstruction,
- createInitializeTransferHookInstruction,
- createMintToInstruction,
- createTransferCheckedWithTransferHookInstruction,
- getAssociatedTokenAddressSync,
- getMintLen,
- } from '@solana/spl-token';
- import { PublicKey } from '@solana/web3.js';
- import { Keypair, SystemProgram } from '@solana/web3.js';
- import { Transaction } from '@solana/web3.js';
- import { TransactionInstruction } from '@solana/web3.js';
- import { BankrunProvider } from 'anchor-bankrun';
- import { assert } from 'chai';
- import { startAnchor } from 'solana-bankrun';
- import type { TransferSwitch } from '../target/types/transfer_switch';
- const IDL = require('../target/idl/transfer_switch.json');
- const PROGRAM_ID = new PublicKey(IDL.address);
- const expectRevert = async (promise: Promise<any>) => {
- try {
- await promise;
- throw new Error('Expected a revert');
- } catch {
- return;
- }
- };
- describe('Transfer switch', async () => {
- const context = await startAnchor('', [{ name: 'transfer_switch', programId: PROGRAM_ID }], []);
- const provider = new BankrunProvider(context);
- const wallet = provider.wallet as anchor.Wallet;
- const program = new anchor.Program<TransferSwitch>(IDL, provider);
- const connection = provider.connection;
- const payer = provider.context.payer;
- const client = provider.context.banksClient;
- // Generate keypair to use as address for the transfer-hook enabled mint
- const mint = Keypair.generate();
- const decimals = 9;
- function newUser(): [Keypair, PublicKey, TransactionInstruction] {
- const user = Keypair.generate();
- const userTokenAccount = getAssociatedTokenAddressSync(mint.publicKey, user.publicKey, false, TOKEN_2022_PROGRAM_ID);
- const createUserTokenAccountIx = createAssociatedTokenAccountInstruction(
- payer.publicKey,
- userTokenAccount,
- user.publicKey,
- mint.publicKey,
- TOKEN_2022_PROGRAM_ID,
- ASSOCIATED_TOKEN_PROGRAM_ID,
- );
- return [user, userTokenAccount, createUserTokenAccountIx];
- }
- // admin config address
- const adminConfigAddress = PublicKey.findProgramAddressSync([Buffer.from('admin-config')], PROGRAM_ID)[0];
- // helper for getting wallet switch
- const walletTransferSwitchAddress = (wallet: PublicKey) => PublicKey.findProgramAddressSync([wallet.toBuffer()], PROGRAM_ID)[0];
- // sender
- const [sender, senderTokenAccount, senderTokenAccountCreateIx] = newUser();
- it('Create Mint Account with Transfer Hook Extension', async () => {
- const extensions = [ExtensionType.TransferHook];
- const mintLen = getMintLen(extensions);
- const lamports = await provider.connection.getMinimumBalanceForRentExemption(mintLen);
- const transaction = new Transaction().add(
- SystemProgram.createAccount({
- fromPubkey: payer.publicKey,
- newAccountPubkey: mint.publicKey,
- space: mintLen,
- lamports: lamports,
- programId: TOKEN_2022_PROGRAM_ID,
- }),
- createInitializeTransferHookInstruction(
- mint.publicKey,
- payer.publicKey,
- program.programId, // Transfer Hook Program ID
- TOKEN_2022_PROGRAM_ID,
- ),
- createInitializeMintInstruction(mint.publicKey, decimals, payer.publicKey, null, TOKEN_2022_PROGRAM_ID),
- );
- transaction.recentBlockhash = context.lastBlockhash;
- transaction.sign(payer, mint);
- await client.processTransaction(transaction);
- });
- // Create the two token accounts for the transfer-hook enabled mint
- // Fund the sender token account with 100 tokens
- it('Create Token Accounts and Mint Tokens', async () => {
- // 100 tokens
- const amount = 100 * 10 ** decimals;
- const transaction = new Transaction().add(
- senderTokenAccountCreateIx, // create sender token account
- createMintToInstruction(mint.publicKey, senderTokenAccount, payer.publicKey, amount, [], TOKEN_2022_PROGRAM_ID),
- );
- transaction.recentBlockhash = context.lastBlockhash;
- transaction.sign(payer);
- await client.processTransaction(transaction);
- });
- // Account to store extra accounts required by the transfer hook instruction
- // This will be called for every mint
- //
- it('Create ExtraAccountMetaList Account', async () => {
- await program.methods
- .initializeExtraAccountMetasList()
- .accounts({
- payer: payer.publicKey,
- tokenMint: mint.publicKey,
- })
- .signers([payer])
- .rpc();
- });
- // Set the account that controls the switches for the wallet
- it('Configure an admin', async () => {
- await program.methods
- .configureAdmin()
- .accounts({
- admin: payer.publicKey,
- newAdmin: payer.publicKey,
- })
- .signers([payer])
- .rpc();
- const adminConfig = await program.account.adminConfig.fetch(adminConfigAddress);
- assert(adminConfig.isInitialised === true, 'admin config not initialised');
- assert(adminConfig.admin.toBase58() === payer.publicKey.toBase58(), 'admin does not match');
- });
- // Account to store extra accounts required by the transfer hook instruction
- it('turn transfers off for sender', async () => {
- await program.methods
- .switch(false)
- .accountsPartial({
- wallet: sender.publicKey,
- admin: payer.publicKey,
- })
- .signers([payer])
- .rpc();
- const walletSwitch = await program.account.transferSwitch.fetch(walletTransferSwitchAddress(sender.publicKey));
- assert(walletSwitch.wallet.toBase58() === sender.publicKey.toBase58(), 'wallet key does not match');
- assert(!walletSwitch.on, 'wallet switch not set to false');
- });
- it('Try transfer, should fail!', async () => {
- // 1 tokens
- const amount = 1 * 10 ** decimals;
- const bigIntAmount = BigInt(amount);
- const [recipient, recipientTokenAccount, recipientTokenAccountCreateIx] = newUser();
- // create the recipient token account ahead of the transfer,
- //
- let transaction = new Transaction().add(
- recipientTokenAccountCreateIx, // create recipient token account
- );
- transaction.recentBlockhash = context.lastBlockhash;
- transaction.sign(payer, recipient);
- client.processTransaction(transaction);
- // Standard token transfer instruction
- const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
- connection,
- senderTokenAccount,
- mint.publicKey,
- recipientTokenAccount,
- sender.publicKey,
- bigIntAmount,
- decimals,
- [],
- 'confirmed',
- TOKEN_2022_PROGRAM_ID,
- );
- transaction = new Transaction().add(
- transferInstruction, // transfer instruction
- );
- transaction.recentBlockhash = context.lastBlockhash;
- transaction.sign(payer, sender);
- // expect the transaction to fail
- //
- expectRevert(client.processTransaction(transaction));
- const recipientTokenAccountData = (await client.getAccount(recipientTokenAccount)).data;
- const recipientBalance = AccountLayout.decode(recipientTokenAccountData).amount;
- assert(recipientBalance === BigInt(0), 'transfer was successful');
- });
- // Account to store extra accounts required by the transfer hook instruction
- it('turn on for sender!', async () => {
- await program.methods
- .switch(true)
- .accountsPartial({
- wallet: sender.publicKey,
- admin: payer.publicKey,
- })
- .signers([payer])
- .rpc();
- const walletSwitch = await program.account.transferSwitch.fetch(walletTransferSwitchAddress(sender.publicKey));
- assert(walletSwitch.wallet.toBase58() === sender.publicKey.toBase58(), 'wallet key does not match');
- assert(walletSwitch.on, 'wallet switch not set to true');
- });
- it('Send successfully', async () => {
- // 1 tokens
- const amount = 1 * 10 ** decimals;
- const bigIntAmount = BigInt(amount);
- const [recipient, recipientTokenAccount, recipientTokenAccountCreateIx] = newUser();
- // Standard token transfer instruction
- const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
- connection,
- senderTokenAccount,
- mint.publicKey,
- recipientTokenAccount,
- sender.publicKey,
- bigIntAmount,
- decimals,
- [],
- 'confirmed',
- TOKEN_2022_PROGRAM_ID,
- );
- const transaction = new Transaction().add(recipientTokenAccountCreateIx, transferInstruction);
- transaction.recentBlockhash = context.lastBlockhash;
- transaction.sign(payer, sender);
- await client.processTransaction(transaction);
- const recipientTokenAccountData = (await client.getAccount(recipientTokenAccount)).data;
- const recipientBalance = AccountLayout.decode(recipientTokenAccountData).amount;
- assert(recipientBalance === bigIntAmount, 'transfer was not successful');
- });
- });
|