transfer-hook.ts 6.9 KB

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