transfer-hook.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import assert from 'node:assert';
  2. import * as anchor from '@coral-xyz/anchor';
  3. import type { Program } from '@coral-xyz/anchor';
  4. import {
  5. ASSOCIATED_TOKEN_PROGRAM_ID,
  6. ExtensionType,
  7. NATIVE_MINT,
  8. TOKEN_2022_PROGRAM_ID,
  9. TOKEN_PROGRAM_ID,
  10. createApproveInstruction,
  11. createAssociatedTokenAccountInstruction,
  12. createInitializeMintInstruction,
  13. createInitializeTransferHookInstruction,
  14. createMintToInstruction,
  15. createSyncNativeInstruction,
  16. createTransferCheckedWithTransferHookInstruction,
  17. getAccount,
  18. getAssociatedTokenAddressSync,
  19. getExtraAccountMetaAddress,
  20. getExtraAccountMetas,
  21. getMint,
  22. getMintLen,
  23. getOrCreateAssociatedTokenAccount,
  24. getTransferHook,
  25. } from '@solana/spl-token';
  26. import { Keypair, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
  27. import type { TransferHook } from '../target/types/transfer_hook';
  28. describe('transfer-hook', () => {
  29. // Configure the client to use the local cluster.
  30. const provider = anchor.AnchorProvider.env();
  31. anchor.setProvider(provider);
  32. const program = anchor.workspace.TransferHook as Program<TransferHook>;
  33. const wallet = provider.wallet as anchor.Wallet;
  34. const connection = provider.connection;
  35. // Generate keypair to use as address for the transfer-hook enabled mint
  36. const mint = new Keypair();
  37. const decimals = 9;
  38. // Sender token account address
  39. const sourceTokenAccount = getAssociatedTokenAddressSync(
  40. mint.publicKey,
  41. wallet.publicKey,
  42. false,
  43. TOKEN_2022_PROGRAM_ID,
  44. ASSOCIATED_TOKEN_PROGRAM_ID,
  45. );
  46. // Recipient token account address
  47. const recipient = Keypair.generate();
  48. const destinationTokenAccount = getAssociatedTokenAddressSync(
  49. mint.publicKey,
  50. recipient.publicKey,
  51. false,
  52. TOKEN_2022_PROGRAM_ID,
  53. ASSOCIATED_TOKEN_PROGRAM_ID,
  54. );
  55. // PDA delegate to transfer wSOL tokens from sender
  56. const [delegatePDA] = PublicKey.findProgramAddressSync([Buffer.from('delegate')], program.programId);
  57. // Sender wSOL token account address
  58. const senderWSolTokenAccount = getAssociatedTokenAddressSync(
  59. NATIVE_MINT, // mint
  60. wallet.publicKey, // owner
  61. );
  62. // Delegate PDA wSOL token account address, to receive wSOL tokens from sender
  63. const delegateWSolTokenAccount = getAssociatedTokenAddressSync(
  64. NATIVE_MINT, // mint
  65. delegatePDA, // owner
  66. true, // allowOwnerOffCurve
  67. );
  68. // Create the two WSol token accounts as part of setup
  69. before(async () => {
  70. // WSol Token Account for sender
  71. await getOrCreateAssociatedTokenAccount(connection, wallet.payer, NATIVE_MINT, wallet.publicKey);
  72. // WSol Token Account for delegate PDA
  73. await getOrCreateAssociatedTokenAccount(connection, wallet.payer, NATIVE_MINT, delegatePDA, true);
  74. });
  75. it('Create Mint Account with Transfer Hook Extension', async () => {
  76. const extensions = [ExtensionType.TransferHook];
  77. const mintLen = getMintLen(extensions);
  78. const lamports = await provider.connection.getMinimumBalanceForRentExemption(mintLen);
  79. const transaction = new Transaction().add(
  80. SystemProgram.createAccount({
  81. fromPubkey: wallet.publicKey,
  82. newAccountPubkey: mint.publicKey,
  83. space: mintLen,
  84. lamports: lamports,
  85. programId: TOKEN_2022_PROGRAM_ID,
  86. }),
  87. createInitializeTransferHookInstruction(
  88. mint.publicKey,
  89. wallet.publicKey,
  90. program.programId, // Transfer Hook Program ID
  91. TOKEN_2022_PROGRAM_ID,
  92. ),
  93. createInitializeMintInstruction(mint.publicKey, decimals, wallet.publicKey, null, TOKEN_2022_PROGRAM_ID),
  94. );
  95. const txSig = await sendAndConfirmTransaction(provider.connection, transaction, [wallet.payer, mint]);
  96. console.log(`Transaction Signature: ${txSig}`);
  97. });
  98. // Create the two token accounts for the transfer-hook enabled mint
  99. // Fund the sender token account with 100 tokens
  100. it('Create Token Accounts and Mint Tokens', async () => {
  101. // 100 tokens
  102. const amount = 100 * 10 ** decimals;
  103. const transaction = new Transaction().add(
  104. createAssociatedTokenAccountInstruction(
  105. wallet.publicKey,
  106. sourceTokenAccount,
  107. wallet.publicKey,
  108. mint.publicKey,
  109. TOKEN_2022_PROGRAM_ID,
  110. ASSOCIATED_TOKEN_PROGRAM_ID,
  111. ),
  112. createAssociatedTokenAccountInstruction(
  113. wallet.publicKey,
  114. destinationTokenAccount,
  115. recipient.publicKey,
  116. mint.publicKey,
  117. TOKEN_2022_PROGRAM_ID,
  118. ASSOCIATED_TOKEN_PROGRAM_ID,
  119. ),
  120. createMintToInstruction(mint.publicKey, sourceTokenAccount, wallet.publicKey, amount, [], TOKEN_2022_PROGRAM_ID),
  121. );
  122. const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true });
  123. console.log(`Transaction Signature: ${txSig}`);
  124. });
  125. // Account to store extra accounts required by the transfer hook instruction
  126. it('Create ExtraAccountMetaList Account', async () => {
  127. const initializeExtraAccountMetaListInstruction = await program.methods
  128. .initializeExtraAccountMetaList()
  129. .accounts({
  130. payer: wallet.publicKey,
  131. mint: mint.publicKey,
  132. })
  133. .instruction();
  134. const transaction = new Transaction().add(initializeExtraAccountMetaListInstruction);
  135. const txSig = await sendAndConfirmTransaction(provider.connection, transaction, [wallet.payer], { skipPreflight: true, commitment: 'confirmed' });
  136. console.log('Transaction Signature:', txSig);
  137. });
  138. it('Transfer Hook with Extra Account Meta', async () => {
  139. // 1 tokens
  140. const amount = 1 * 10 ** decimals;
  141. const bigIntAmount = BigInt(amount);
  142. // Instruction for sender to fund their WSol token account
  143. const solTransferInstruction = SystemProgram.transfer({
  144. fromPubkey: wallet.publicKey,
  145. toPubkey: senderWSolTokenAccount,
  146. lamports: amount,
  147. });
  148. // Approve delegate PDA to transfer WSol tokens from sender WSol token account
  149. const approveInstruction = createApproveInstruction(senderWSolTokenAccount, delegatePDA, wallet.publicKey, amount, [], TOKEN_PROGRAM_ID);
  150. // Sync sender WSol token account
  151. const syncWrappedSolInstruction = createSyncNativeInstruction(senderWSolTokenAccount);
  152. const mintInfo = await getMint(connection, mint.publicKey, 'confirmed', TOKEN_2022_PROGRAM_ID);
  153. const transferHook = getTransferHook(mintInfo);
  154. if (transferHook != null) {
  155. console.log(`Transfer hook program found: ${JSON.stringify(transferHook, null, 2)}`);
  156. }
  157. const extraAccountsAccount = getExtraAccountMetaAddress(mint.publicKey, transferHook.programId);
  158. const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, 'confirmed');
  159. const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);
  160. for (const extraAccountMeta of extraAccountMetas) {
  161. console.log(`Extra account meta: ${JSON.stringify(extraAccountMeta, null, 2)}`);
  162. }
  163. // Standard token transfer instruction
  164. const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
  165. connection,
  166. sourceTokenAccount,
  167. mint.publicKey,
  168. destinationTokenAccount,
  169. wallet.publicKey,
  170. bigIntAmount,
  171. decimals,
  172. [],
  173. 'confirmed',
  174. TOKEN_2022_PROGRAM_ID,
  175. );
  176. console.log('Pushed keys:', JSON.stringify(transferInstruction.keys, null, 2));
  177. const transaction = new Transaction().add(solTransferInstruction, syncWrappedSolInstruction, approveInstruction, transferInstruction);
  178. const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true });
  179. console.log('Transfer Signature:', txSig);
  180. const tokenAccount = await getAccount(connection, delegateWSolTokenAccount);
  181. assert.equal(Number(tokenAccount.amount), amount);
  182. });
  183. });