transfer-hook.ts 6.9 KB

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