transfer-hook.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. // PDA delegate to transfer wSOL tokens from sender
  62. const [delegatePDA] = PublicKey.findProgramAddressSync(
  63. [Buffer.from("delegate")],
  64. program.programId
  65. );
  66. // Sender wSOL token account address
  67. const senderWSolTokenAccount = getAssociatedTokenAddressSync(
  68. NATIVE_MINT, // mint
  69. wallet.publicKey // owner
  70. );
  71. // Delegate PDA wSOL token account address, to receive wSOL tokens from sender
  72. const delegateWSolTokenAccount = getAssociatedTokenAddressSync(
  73. NATIVE_MINT, // mint
  74. delegatePDA, // owner
  75. true // allowOwnerOffCurve
  76. );
  77. // Create the two WSol token accounts as part of setup
  78. before(async () => {
  79. // WSol Token Account for sender
  80. await getOrCreateAssociatedTokenAccount(
  81. connection,
  82. wallet.payer,
  83. NATIVE_MINT,
  84. wallet.publicKey
  85. );
  86. // WSol Token Account for delegate PDA
  87. await getOrCreateAssociatedTokenAccount(
  88. connection,
  89. wallet.payer,
  90. NATIVE_MINT,
  91. delegatePDA,
  92. true
  93. );
  94. });
  95. it("Create Mint Account with Transfer Hook Extension", async () => {
  96. const extensions = [ExtensionType.TransferHook];
  97. const mintLen = getMintLen(extensions);
  98. const lamports =
  99. await provider.connection.getMinimumBalanceForRentExemption(mintLen);
  100. const transaction = new Transaction().add(
  101. SystemProgram.createAccount({
  102. fromPubkey: wallet.publicKey,
  103. newAccountPubkey: mint.publicKey,
  104. space: mintLen,
  105. lamports: lamports,
  106. programId: TOKEN_2022_PROGRAM_ID,
  107. }),
  108. createInitializeTransferHookInstruction(
  109. mint.publicKey,
  110. wallet.publicKey,
  111. program.programId, // Transfer Hook Program ID
  112. TOKEN_2022_PROGRAM_ID
  113. ),
  114. createInitializeMintInstruction(
  115. mint.publicKey,
  116. decimals,
  117. wallet.publicKey,
  118. null,
  119. TOKEN_2022_PROGRAM_ID
  120. )
  121. );
  122. const txSig = await sendAndConfirmTransaction(
  123. provider.connection,
  124. transaction,
  125. [wallet.payer, mint]
  126. );
  127. console.log(`Transaction Signature: ${txSig}`);
  128. });
  129. // Create the two token accounts for the transfer-hook enabled mint
  130. // Fund the sender token account with 100 tokens
  131. it("Create Token Accounts and Mint Tokens", async () => {
  132. // 100 tokens
  133. const amount = 100 * 10 ** decimals;
  134. const transaction = new Transaction().add(
  135. createAssociatedTokenAccountInstruction(
  136. wallet.publicKey,
  137. sourceTokenAccount,
  138. wallet.publicKey,
  139. mint.publicKey,
  140. TOKEN_2022_PROGRAM_ID,
  141. ASSOCIATED_TOKEN_PROGRAM_ID
  142. ),
  143. createAssociatedTokenAccountInstruction(
  144. wallet.publicKey,
  145. destinationTokenAccount,
  146. recipient.publicKey,
  147. mint.publicKey,
  148. TOKEN_2022_PROGRAM_ID,
  149. ASSOCIATED_TOKEN_PROGRAM_ID
  150. ),
  151. createMintToInstruction(
  152. mint.publicKey,
  153. sourceTokenAccount,
  154. wallet.publicKey,
  155. amount,
  156. [],
  157. TOKEN_2022_PROGRAM_ID
  158. )
  159. );
  160. const txSig = await sendAndConfirmTransaction(
  161. connection,
  162. transaction,
  163. [wallet.payer],
  164. { skipPreflight: true }
  165. );
  166. console.log(`Transaction Signature: ${txSig}`);
  167. });
  168. // Account to store extra accounts required by the transfer hook instruction
  169. it("Create ExtraAccountMetaList Account", async () => {
  170. const initializeExtraAccountMetaListInstruction = await program.methods
  171. .initializeExtraAccountMetaList()
  172. .accounts({
  173. payer: wallet.publicKey,
  174. mint: mint.publicKey,
  175. })
  176. .instruction();
  177. const transaction = new Transaction().add(
  178. initializeExtraAccountMetaListInstruction
  179. );
  180. const txSig = await sendAndConfirmTransaction(
  181. provider.connection,
  182. transaction,
  183. [wallet.payer],
  184. { skipPreflight: true, commitment: "confirmed" }
  185. );
  186. console.log("Transaction Signature:", txSig);
  187. });
  188. it("Transfer Hook with Extra Account Meta", async () => {
  189. // 1 tokens
  190. const amount = 1 * 10 ** decimals;
  191. const bigIntAmount = BigInt(amount);
  192. // Instruction for sender to fund their WSol token account
  193. const solTransferInstruction = SystemProgram.transfer({
  194. fromPubkey: wallet.publicKey,
  195. toPubkey: senderWSolTokenAccount,
  196. lamports: amount,
  197. });
  198. // Approve delegate PDA to transfer WSol tokens from sender WSol token account
  199. const approveInstruction = createApproveInstruction(
  200. senderWSolTokenAccount,
  201. delegatePDA,
  202. wallet.publicKey,
  203. amount,
  204. [],
  205. TOKEN_PROGRAM_ID
  206. );
  207. // Sync sender WSol token account
  208. const syncWrappedSolInstruction = createSyncNativeInstruction(
  209. senderWSolTokenAccount
  210. );
  211. const mintInfo = await getMint(
  212. connection,
  213. mint.publicKey,
  214. "confirmed",
  215. TOKEN_2022_PROGRAM_ID
  216. );
  217. const transferHook = getTransferHook(mintInfo);
  218. if (transferHook != null) {
  219. console.log(
  220. "Transfer hook program found: " + JSON.stringify(transferHook, null, 2)
  221. );
  222. }
  223. const extraAccountsAccount = getExtraAccountMetaAddress(
  224. mint.publicKey,
  225. transferHook.programId
  226. );
  227. const extraAccountsInfo = await connection.getAccountInfo(
  228. extraAccountsAccount,
  229. "confirmed"
  230. );
  231. const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);
  232. for (const extraAccountMeta of extraAccountMetas) {
  233. console.log(
  234. "Extra account meta: " + JSON.stringify(extraAccountMeta, null, 2)
  235. );
  236. }
  237. // Standard token transfer instruction
  238. const transferInstruction =
  239. await createTransferCheckedWithTransferHookInstruction(
  240. connection,
  241. sourceTokenAccount,
  242. mint.publicKey,
  243. destinationTokenAccount,
  244. wallet.publicKey,
  245. bigIntAmount,
  246. decimals,
  247. [],
  248. "confirmed",
  249. TOKEN_2022_PROGRAM_ID
  250. );
  251. console.log(
  252. "Pushed keys:",
  253. JSON.stringify(transferInstruction.keys, null, 2)
  254. );
  255. const transaction = new Transaction().add(
  256. solTransferInstruction,
  257. syncWrappedSolInstruction,
  258. approveInstruction,
  259. transferInstruction
  260. );
  261. const txSig = await sendAndConfirmTransaction(
  262. connection,
  263. transaction,
  264. [wallet.payer],
  265. { skipPreflight: true }
  266. );
  267. console.log("Transfer Signature:", txSig);
  268. const tokenAccount = await getAccount(connection, delegateWSolTokenAccount);
  269. assert.equal(Number(tokenAccount.amount), amount);
  270. });
  271. });