transfer-hook.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import * as anchor from '@coral-xyz/anchor';
  2. import type { Program } from '@coral-xyz/anchor';
  3. import {
  4. ASSOCIATED_TOKEN_PROGRAM_ID,
  5. ExtensionType,
  6. TOKEN_2022_PROGRAM_ID,
  7. createAssociatedTokenAccountInstruction,
  8. createInitializeMintInstruction,
  9. createInitializeTransferHookInstruction,
  10. createMintToInstruction,
  11. createTransferCheckedWithTransferHookInstruction,
  12. getAssociatedTokenAddressSync,
  13. getMintLen,
  14. } from '@solana/spl-token';
  15. import { Keypair, PublicKey, SendTransactionError, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
  16. import { BN } from 'bn.js';
  17. import { expect } from 'chai';
  18. import chai from 'chai';
  19. import chaiAsPromised from 'chai-as-promised';
  20. import type { TransferHook } from '../target/types/transfer_hook';
  21. chai.use(chaiAsPromised);
  22. describe('transfer-hook', () => {
  23. // Configure the client to use the local cluster.
  24. const provider = anchor.AnchorProvider.env();
  25. anchor.setProvider(provider);
  26. const program = anchor.workspace.TransferHook as Program<TransferHook>;
  27. const wallet = provider.wallet as anchor.Wallet;
  28. const connection = provider.connection;
  29. // Generate keypair to use as address for the transfer-hook enabled mint
  30. const mint = new Keypair();
  31. const decimals = 9;
  32. // Sender token account address
  33. const sourceTokenAccount = getAssociatedTokenAddressSync(
  34. mint.publicKey,
  35. wallet.publicKey,
  36. false,
  37. TOKEN_2022_PROGRAM_ID,
  38. ASSOCIATED_TOKEN_PROGRAM_ID,
  39. );
  40. // Recipient token account address
  41. const recipient = Keypair.generate();
  42. const destinationTokenAccount = getAssociatedTokenAddressSync(
  43. mint.publicKey,
  44. recipient.publicKey,
  45. false,
  46. TOKEN_2022_PROGRAM_ID,
  47. ASSOCIATED_TOKEN_PROGRAM_ID,
  48. );
  49. // ExtraAccountMetaList address
  50. // Store extra accounts required by the custom transfer hook instruction
  51. const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync(
  52. [Buffer.from('extra-account-metas'), mint.publicKey.toBuffer()],
  53. program.programId,
  54. );
  55. const [counterPDA] = PublicKey.findProgramAddressSync([Buffer.from('counter'), wallet.publicKey.toBuffer()], program.programId);
  56. it('Create Mint Account with Transfer Hook Extension', async () => {
  57. const extensions = [ExtensionType.TransferHook];
  58. const mintLen = getMintLen(extensions);
  59. const lamports = await provider.connection.getMinimumBalanceForRentExemption(mintLen);
  60. const transaction = new Transaction().add(
  61. SystemProgram.createAccount({
  62. fromPubkey: wallet.publicKey,
  63. newAccountPubkey: mint.publicKey,
  64. space: mintLen,
  65. lamports: lamports,
  66. programId: TOKEN_2022_PROGRAM_ID,
  67. }),
  68. createInitializeTransferHookInstruction(
  69. mint.publicKey,
  70. wallet.publicKey,
  71. program.programId, // Transfer Hook Program ID
  72. TOKEN_2022_PROGRAM_ID,
  73. ),
  74. createInitializeMintInstruction(mint.publicKey, decimals, wallet.publicKey, null, TOKEN_2022_PROGRAM_ID),
  75. );
  76. const txSig = await sendAndConfirmTransaction(provider.connection, transaction, [wallet.payer, mint], {
  77. skipPreflight: true,
  78. commitment: 'finalized',
  79. });
  80. const txDetails = await program.provider.connection.getTransaction(txSig, {
  81. maxSupportedTransactionVersion: 0,
  82. commitment: 'confirmed',
  83. });
  84. console.log(txDetails.meta.logMessages);
  85. console.log(`Transaction Signature: ${txSig}`);
  86. });
  87. // Create the two token accounts for the transfer-hook enabled mint
  88. // Fund the sender token account with 100 tokens
  89. it('Create Token Accounts and Mint Tokens', async () => {
  90. // 100 tokens
  91. const amount = 100 * 10 ** decimals;
  92. const transaction = new Transaction().add(
  93. createAssociatedTokenAccountInstruction(
  94. wallet.publicKey,
  95. sourceTokenAccount,
  96. wallet.publicKey,
  97. mint.publicKey,
  98. TOKEN_2022_PROGRAM_ID,
  99. ASSOCIATED_TOKEN_PROGRAM_ID,
  100. ),
  101. createAssociatedTokenAccountInstruction(
  102. wallet.publicKey,
  103. destinationTokenAccount,
  104. recipient.publicKey,
  105. mint.publicKey,
  106. TOKEN_2022_PROGRAM_ID,
  107. ASSOCIATED_TOKEN_PROGRAM_ID,
  108. ),
  109. createMintToInstruction(mint.publicKey, sourceTokenAccount, wallet.publicKey, amount, [], TOKEN_2022_PROGRAM_ID),
  110. );
  111. const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true });
  112. console.log(`Transaction Signature: ${txSig}`);
  113. });
  114. // Account to store extra accounts required by the transfer hook instruction
  115. it('Create ExtraAccountMetaList Account', async () => {
  116. const initializeExtraAccountMetaListInstruction = await program.methods
  117. .initializeExtraAccountMetaList()
  118. .accounts({
  119. mint: mint.publicKey,
  120. })
  121. .instruction();
  122. const transaction = new Transaction().add(initializeExtraAccountMetaListInstruction);
  123. const txSig = await sendAndConfirmTransaction(provider.connection, transaction, [wallet.payer], { skipPreflight: true, commitment: 'confirmed' });
  124. console.log('Transaction Signature:', txSig);
  125. });
  126. it('Transfer Hook with Extra Account Meta', async () => {
  127. // 1 tokens
  128. const amount = 1 * 10 ** decimals;
  129. const amountBigInt = BigInt(amount);
  130. const transferInstructionWithHelper = await createTransferCheckedWithTransferHookInstruction(
  131. connection,
  132. sourceTokenAccount,
  133. mint.publicKey,
  134. destinationTokenAccount,
  135. wallet.publicKey,
  136. amountBigInt,
  137. decimals,
  138. [],
  139. 'confirmed',
  140. TOKEN_2022_PROGRAM_ID,
  141. );
  142. console.log(`Extra accounts meta: ${extraAccountMetaListPDA}`);
  143. console.log(`Counter PDA: ${counterPDA}`);
  144. console.log(`Transfer Instruction: ${JSON.stringify(transferInstructionWithHelper)}`);
  145. const transaction = new Transaction().add(transferInstructionWithHelper);
  146. const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true });
  147. console.log('Transfer Signature:', txSig);
  148. });
  149. it('Try call transfer hook without transfer', async () => {
  150. const transferHookIx = await program.methods
  151. .transferHook(new BN(1))
  152. .accounts({
  153. sourceToken: sourceTokenAccount,
  154. mint: mint.publicKey,
  155. destinationToken: destinationTokenAccount,
  156. owner: wallet.publicKey,
  157. })
  158. .instruction();
  159. const transaction = new Transaction().add(transferHookIx);
  160. const sendPromise = sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: false });
  161. await expect(sendPromise).to.eventually.be.rejectedWith(SendTransactionError, program.idl.errors[1].msg);
  162. });
  163. });