transfer-hook.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import * as anchor from "@coral-xyz/anchor";
  2. import { Program } from "@coral-xyz/anchor";
  3. import { TransferHook } from "../target/types/transfer_hook";
  4. import {
  5. PublicKey,
  6. SystemProgram,
  7. Transaction,
  8. sendAndConfirmTransaction,
  9. Keypair,
  10. } from "@solana/web3.js";
  11. import {
  12. ExtensionType,
  13. TOKEN_2022_PROGRAM_ID,
  14. getMintLen,
  15. createInitializeMintInstruction,
  16. createInitializeTransferHookInstruction,
  17. ASSOCIATED_TOKEN_PROGRAM_ID,
  18. createAssociatedTokenAccountInstruction,
  19. createMintToInstruction,
  20. getAssociatedTokenAddressSync,
  21. createApproveInstruction,
  22. createSyncNativeInstruction,
  23. NATIVE_MINT,
  24. TOKEN_PROGRAM_ID,
  25. getAccount,
  26. getOrCreateAssociatedTokenAccount,
  27. createTransferCheckedWithTransferHookInstruction,
  28. getMint,
  29. getTransferHook,
  30. getExtraAccountMetaAddress,
  31. getExtraAccountMetas,
  32. } from "@solana/spl-token";
  33. import assert from "assert";
  34. describe("transfer-hook", () => {
  35. // Configure the client to use the local cluster.
  36. const provider = anchor.AnchorProvider.env();
  37. anchor.setProvider(provider);
  38. const program = anchor.workspace.TransferHook as Program<TransferHook>;
  39. const wallet = provider.wallet as anchor.Wallet;
  40. const connection = provider.connection;
  41. // Generate keypair to use as address for the transfer-hook enabled mint
  42. const mint = new Keypair();
  43. const decimals = 9;
  44. // Sender token account address
  45. const sourceTokenAccount = getAssociatedTokenAddressSync(
  46. mint.publicKey,
  47. wallet.publicKey,
  48. false,
  49. TOKEN_2022_PROGRAM_ID,
  50. ASSOCIATED_TOKEN_PROGRAM_ID
  51. );
  52. // Recipient token account address
  53. const recipient = Keypair.generate();
  54. const destinationTokenAccount = getAssociatedTokenAddressSync(
  55. mint.publicKey,
  56. recipient.publicKey,
  57. false,
  58. TOKEN_2022_PROGRAM_ID,
  59. ASSOCIATED_TOKEN_PROGRAM_ID
  60. );
  61. // ExtraAccountMetaList address
  62. // Store extra accounts required by the custom transfer hook instruction
  63. const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync(
  64. [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
  65. program.programId
  66. );
  67. const [counterPDA] = PublicKey.findProgramAddressSync(
  68. [Buffer.from("counter")],
  69. program.programId
  70. );
  71. // PDA delegate to transfer wSOL tokens from sender
  72. const [delegatePDA] = PublicKey.findProgramAddressSync(
  73. [Buffer.from("delegate")],
  74. program.programId
  75. );
  76. // Sender wSOL token account address
  77. const senderWSolTokenAccount = getAssociatedTokenAddressSync(
  78. NATIVE_MINT, // mint
  79. wallet.publicKey // owner
  80. );
  81. // Delegate PDA wSOL token account address, to receive wSOL tokens from sender
  82. const delegateWSolTokenAccount = getAssociatedTokenAddressSync(
  83. NATIVE_MINT, // mint
  84. delegatePDA, // owner
  85. true // allowOwnerOffCurve
  86. );
  87. // Create the two WSol token accounts as part of setup
  88. before(async () => {
  89. // WSol Token Account for sender
  90. await getOrCreateAssociatedTokenAccount(
  91. connection,
  92. wallet.payer,
  93. NATIVE_MINT,
  94. wallet.publicKey
  95. );
  96. // WSol Token Account for delegate PDA
  97. await getOrCreateAssociatedTokenAccount(
  98. connection,
  99. wallet.payer,
  100. NATIVE_MINT,
  101. delegatePDA,
  102. true
  103. );
  104. });
  105. it("Create Mint Account with Transfer Hook Extension", async () => {
  106. const extensions = [ExtensionType.TransferHook];
  107. const mintLen = getMintLen(extensions);
  108. const lamports =
  109. await provider.connection.getMinimumBalanceForRentExemption(mintLen);
  110. const transaction = new Transaction().add(
  111. SystemProgram.createAccount({
  112. fromPubkey: wallet.publicKey,
  113. newAccountPubkey: mint.publicKey,
  114. space: mintLen,
  115. lamports: lamports,
  116. programId: TOKEN_2022_PROGRAM_ID,
  117. }),
  118. createInitializeTransferHookInstruction(
  119. mint.publicKey,
  120. wallet.publicKey,
  121. program.programId, // Transfer Hook Program ID
  122. TOKEN_2022_PROGRAM_ID
  123. ),
  124. createInitializeMintInstruction(
  125. mint.publicKey,
  126. decimals,
  127. wallet.publicKey,
  128. null,
  129. TOKEN_2022_PROGRAM_ID
  130. )
  131. );
  132. const txSig = await sendAndConfirmTransaction(
  133. provider.connection,
  134. transaction,
  135. [wallet.payer, mint]
  136. );
  137. console.log(`Transaction Signature: ${txSig}`);
  138. });
  139. // Create the two token accounts for the transfer-hook enabled mint
  140. // Fund the sender token account with 100 tokens
  141. it("Create Token Accounts and Mint Tokens", async () => {
  142. // 100 tokens
  143. const amount = 100 * 10 ** decimals;
  144. const transaction = new Transaction().add(
  145. createAssociatedTokenAccountInstruction(
  146. wallet.publicKey,
  147. sourceTokenAccount,
  148. wallet.publicKey,
  149. mint.publicKey,
  150. TOKEN_2022_PROGRAM_ID,
  151. ASSOCIATED_TOKEN_PROGRAM_ID
  152. ),
  153. createAssociatedTokenAccountInstruction(
  154. wallet.publicKey,
  155. destinationTokenAccount,
  156. recipient.publicKey,
  157. mint.publicKey,
  158. TOKEN_2022_PROGRAM_ID,
  159. ASSOCIATED_TOKEN_PROGRAM_ID
  160. ),
  161. createMintToInstruction(
  162. mint.publicKey,
  163. sourceTokenAccount,
  164. wallet.publicKey,
  165. amount,
  166. [],
  167. TOKEN_2022_PROGRAM_ID
  168. )
  169. );
  170. const txSig = await sendAndConfirmTransaction(
  171. connection,
  172. transaction,
  173. [wallet.payer],
  174. { skipPreflight: true }
  175. );
  176. console.log(`Transaction Signature: ${txSig}`);
  177. });
  178. // Account to store extra accounts required by the transfer hook instruction
  179. it("Create ExtraAccountMetaList Account", async () => {
  180. const initializeExtraAccountMetaListInstruction = await program.methods
  181. .initializeExtraAccountMetaList()
  182. .accounts({
  183. payer: wallet.publicKey,
  184. extraAccountMetaList: extraAccountMetaListPDA,
  185. mint: mint.publicKey,
  186. wsolMint: NATIVE_MINT,
  187. counterAccount: counterPDA,
  188. tokenProgram: TOKEN_PROGRAM_ID,
  189. associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
  190. })
  191. .instruction();
  192. const transaction = new Transaction().add(
  193. initializeExtraAccountMetaListInstruction
  194. );
  195. const txSig = await sendAndConfirmTransaction(
  196. provider.connection,
  197. transaction,
  198. [wallet.payer],
  199. { skipPreflight: true, commitment : "confirmed"}
  200. );
  201. console.log("Transaction Signature:", txSig);
  202. });
  203. it("Transfer Hook with Extra Account Meta", async () => {
  204. // 1 tokens
  205. const amount = 1 * 10 ** decimals;
  206. const bigIntAmount = BigInt(amount);
  207. // Instruction for sender to fund their WSol token account
  208. const solTransferInstruction = SystemProgram.transfer({
  209. fromPubkey: wallet.publicKey,
  210. toPubkey: senderWSolTokenAccount,
  211. lamports: amount,
  212. });
  213. // Approve delegate PDA to transfer WSol tokens from sender WSol token account
  214. const approveInstruction = createApproveInstruction(
  215. senderWSolTokenAccount,
  216. delegatePDA,
  217. wallet.publicKey,
  218. amount,
  219. [],
  220. TOKEN_PROGRAM_ID
  221. );
  222. // Sync sender WSol token account
  223. const syncWrappedSolInstruction = createSyncNativeInstruction(
  224. senderWSolTokenAccount
  225. );
  226. const mintInfo = await getMint(connection, mint.publicKey, "confirmed", TOKEN_2022_PROGRAM_ID);
  227. const transferHook = getTransferHook(mintInfo);
  228. if (transferHook != null) {
  229. console.log("Transfer hook not found" + JSON.stringify(transferHook));
  230. }
  231. const extraAccountsAccount = getExtraAccountMetaAddress(mint.publicKey, transferHook.programId);
  232. const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, "confirmed");
  233. const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);
  234. for (const extraAccountMeta of extraAccountMetas) {
  235. console.log("Extra account meta: " + JSON.stringify(extraAccountMeta));
  236. }
  237. // Standard token transfer instruction
  238. const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
  239. connection,
  240. sourceTokenAccount,
  241. mint.publicKey,
  242. destinationTokenAccount,
  243. wallet.publicKey,
  244. bigIntAmount,
  245. decimals,
  246. [],
  247. "confirmed",
  248. TOKEN_2022_PROGRAM_ID
  249. );
  250. console.log("Pushed keys:", JSON.stringify(transferInstruction.keys));
  251. const transaction = new Transaction().add(
  252. solTransferInstruction,
  253. syncWrappedSolInstruction,
  254. approveInstruction,
  255. transferInstruction
  256. );
  257. const txSig = await sendAndConfirmTransaction(
  258. connection,
  259. transaction,
  260. [wallet.payer],
  261. { skipPreflight: true }
  262. );
  263. console.log("Transfer Signature:", txSig);
  264. const tokenAccount = await getAccount(connection, delegateWSolTokenAccount);
  265. assert.equal(Number(tokenAccount.amount), amount / 2);
  266. });
  267. });