transfer-switch.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import { describe, it } from 'node:test';
  2. import * as anchor from '@coral-xyz/anchor';
  3. import {
  4. ASSOCIATED_TOKEN_PROGRAM_ID,
  5. AccountLayout,
  6. ExtensionType,
  7. TOKEN_2022_PROGRAM_ID,
  8. createAssociatedTokenAccountInstruction,
  9. createInitializeMintInstruction,
  10. createInitializeTransferHookInstruction,
  11. createMintToInstruction,
  12. createTransferCheckedWithTransferHookInstruction,
  13. getAssociatedTokenAddressSync,
  14. getMintLen,
  15. } from '@solana/spl-token';
  16. import { PublicKey } from '@solana/web3.js';
  17. import { Keypair, SystemProgram } from '@solana/web3.js';
  18. import { Transaction } from '@solana/web3.js';
  19. import { TransactionInstruction } from '@solana/web3.js';
  20. import { BankrunProvider } from 'anchor-bankrun';
  21. import { assert } from 'chai';
  22. import { startAnchor } from 'solana-bankrun';
  23. import type { TransferSwitch } from '../target/types/transfer_switch';
  24. const IDL = require('../target/idl/transfer_switch.json');
  25. const PROGRAM_ID = new PublicKey(IDL.address);
  26. const expectRevert = async (promise: Promise<any>) => {
  27. try {
  28. await promise;
  29. throw new Error('Expected a revert');
  30. } catch {
  31. return;
  32. }
  33. };
  34. describe('Transfer switch', async () => {
  35. const context = await startAnchor('', [{ name: 'transfer_switch', programId: PROGRAM_ID }], []);
  36. const provider = new BankrunProvider(context);
  37. const wallet = provider.wallet as anchor.Wallet;
  38. const program = new anchor.Program<TransferSwitch>(IDL, provider);
  39. const connection = provider.connection;
  40. const payer = provider.context.payer;
  41. const client = provider.context.banksClient;
  42. // Generate keypair to use as address for the transfer-hook enabled mint
  43. const mint = Keypair.generate();
  44. const decimals = 9;
  45. function newUser(): [Keypair, PublicKey, TransactionInstruction] {
  46. const user = Keypair.generate();
  47. const userTokenAccount = getAssociatedTokenAddressSync(mint.publicKey, user.publicKey, false, TOKEN_2022_PROGRAM_ID);
  48. const createUserTokenAccountIx = createAssociatedTokenAccountInstruction(
  49. payer.publicKey,
  50. userTokenAccount,
  51. user.publicKey,
  52. mint.publicKey,
  53. TOKEN_2022_PROGRAM_ID,
  54. ASSOCIATED_TOKEN_PROGRAM_ID,
  55. );
  56. return [user, userTokenAccount, createUserTokenAccountIx];
  57. }
  58. // admin config address
  59. const adminConfigAddress = PublicKey.findProgramAddressSync([Buffer.from('admin-config')], PROGRAM_ID)[0];
  60. // helper for getting wallet switch
  61. const walletTransferSwitchAddress = (wallet: PublicKey) => PublicKey.findProgramAddressSync([wallet.toBuffer()], PROGRAM_ID)[0];
  62. // sender
  63. const [sender, senderTokenAccount, senderTokenAccountCreateIx] = newUser();
  64. it('Create Mint Account with Transfer Hook Extension', async () => {
  65. const extensions = [ExtensionType.TransferHook];
  66. const mintLen = getMintLen(extensions);
  67. const lamports = await provider.connection.getMinimumBalanceForRentExemption(mintLen);
  68. const transaction = new Transaction().add(
  69. SystemProgram.createAccount({
  70. fromPubkey: payer.publicKey,
  71. newAccountPubkey: mint.publicKey,
  72. space: mintLen,
  73. lamports: lamports,
  74. programId: TOKEN_2022_PROGRAM_ID,
  75. }),
  76. createInitializeTransferHookInstruction(
  77. mint.publicKey,
  78. payer.publicKey,
  79. program.programId, // Transfer Hook Program ID
  80. TOKEN_2022_PROGRAM_ID,
  81. ),
  82. createInitializeMintInstruction(mint.publicKey, decimals, payer.publicKey, null, TOKEN_2022_PROGRAM_ID),
  83. );
  84. transaction.recentBlockhash = context.lastBlockhash;
  85. transaction.sign(payer, mint);
  86. await client.processTransaction(transaction);
  87. });
  88. // Create the two token accounts for the transfer-hook enabled mint
  89. // Fund the sender token account with 100 tokens
  90. it('Create Token Accounts and Mint Tokens', async () => {
  91. // 100 tokens
  92. const amount = 100 * 10 ** decimals;
  93. const transaction = new Transaction().add(
  94. senderTokenAccountCreateIx, // create sender token account
  95. createMintToInstruction(mint.publicKey, senderTokenAccount, payer.publicKey, amount, [], TOKEN_2022_PROGRAM_ID),
  96. );
  97. transaction.recentBlockhash = context.lastBlockhash;
  98. transaction.sign(payer);
  99. await client.processTransaction(transaction);
  100. });
  101. // Account to store extra accounts required by the transfer hook instruction
  102. // This will be called for every mint
  103. //
  104. it('Create ExtraAccountMetaList Account', async () => {
  105. await program.methods
  106. .initializeExtraAccountMetasList()
  107. .accounts({
  108. payer: payer.publicKey,
  109. tokenMint: mint.publicKey,
  110. })
  111. .signers([payer])
  112. .rpc();
  113. });
  114. // Set the account that controls the switches for the wallet
  115. it('Configure an admin', async () => {
  116. await program.methods
  117. .configureAdmin()
  118. .accounts({
  119. admin: payer.publicKey,
  120. newAdmin: payer.publicKey,
  121. })
  122. .signers([payer])
  123. .rpc();
  124. const adminConfig = await program.account.adminConfig.fetch(adminConfigAddress);
  125. assert(adminConfig.isInitialised === true, 'admin config not initialised');
  126. assert(adminConfig.admin.toBase58() === payer.publicKey.toBase58(), 'admin does not match');
  127. });
  128. // Account to store extra accounts required by the transfer hook instruction
  129. it('turn transfers off for sender', async () => {
  130. await program.methods
  131. .switch(false)
  132. .accountsPartial({
  133. wallet: sender.publicKey,
  134. admin: payer.publicKey,
  135. })
  136. .signers([payer])
  137. .rpc();
  138. const walletSwitch = await program.account.transferSwitch.fetch(walletTransferSwitchAddress(sender.publicKey));
  139. assert(walletSwitch.wallet.toBase58() === sender.publicKey.toBase58(), 'wallet key does not match');
  140. assert(!walletSwitch.on, 'wallet switch not set to false');
  141. });
  142. it('Try transfer, should fail!', async () => {
  143. // 1 tokens
  144. const amount = 1 * 10 ** decimals;
  145. const bigIntAmount = BigInt(amount);
  146. const [recipient, recipientTokenAccount, recipientTokenAccountCreateIx] = newUser();
  147. // create the recipient token account ahead of the transfer,
  148. //
  149. let transaction = new Transaction().add(
  150. recipientTokenAccountCreateIx, // create recipient token account
  151. );
  152. transaction.recentBlockhash = context.lastBlockhash;
  153. transaction.sign(payer, recipient);
  154. client.processTransaction(transaction);
  155. // Standard token transfer instruction
  156. const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
  157. connection,
  158. senderTokenAccount,
  159. mint.publicKey,
  160. recipientTokenAccount,
  161. sender.publicKey,
  162. bigIntAmount,
  163. decimals,
  164. [],
  165. 'confirmed',
  166. TOKEN_2022_PROGRAM_ID,
  167. );
  168. transaction = new Transaction().add(
  169. transferInstruction, // transfer instruction
  170. );
  171. transaction.recentBlockhash = context.lastBlockhash;
  172. transaction.sign(payer, sender);
  173. // expect the transaction to fail
  174. //
  175. expectRevert(client.processTransaction(transaction));
  176. const recipientTokenAccountData = (await client.getAccount(recipientTokenAccount)).data;
  177. const recipientBalance = AccountLayout.decode(recipientTokenAccountData).amount;
  178. assert(recipientBalance === BigInt(0), 'transfer was successful');
  179. });
  180. // Account to store extra accounts required by the transfer hook instruction
  181. it('turn on for sender!', async () => {
  182. await program.methods
  183. .switch(true)
  184. .accountsPartial({
  185. wallet: sender.publicKey,
  186. admin: payer.publicKey,
  187. })
  188. .signers([payer])
  189. .rpc();
  190. const walletSwitch = await program.account.transferSwitch.fetch(walletTransferSwitchAddress(sender.publicKey));
  191. assert(walletSwitch.wallet.toBase58() === sender.publicKey.toBase58(), 'wallet key does not match');
  192. assert(walletSwitch.on, 'wallet switch not set to true');
  193. });
  194. it('Send successfully', async () => {
  195. // 1 tokens
  196. const amount = 1 * 10 ** decimals;
  197. const bigIntAmount = BigInt(amount);
  198. const [recipient, recipientTokenAccount, recipientTokenAccountCreateIx] = newUser();
  199. // Standard token transfer instruction
  200. const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
  201. connection,
  202. senderTokenAccount,
  203. mint.publicKey,
  204. recipientTokenAccount,
  205. sender.publicKey,
  206. bigIntAmount,
  207. decimals,
  208. [],
  209. 'confirmed',
  210. TOKEN_2022_PROGRAM_ID,
  211. );
  212. const transaction = new Transaction().add(recipientTokenAccountCreateIx, transferInstruction);
  213. transaction.recentBlockhash = context.lastBlockhash;
  214. transaction.sign(payer, sender);
  215. await client.processTransaction(transaction);
  216. const recipientTokenAccountData = (await client.getAccount(recipientTokenAccount)).data;
  217. const recipientBalance = AccountLayout.decode(recipientTokenAccountData).amount;
  218. assert(recipientBalance === bigIntAmount, 'transfer was not successful');
  219. });
  220. });