transfer-hook.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import * as anchor from "@coral-xyz/anchor";
  2. import { Program } from "@coral-xyz/anchor";
  3. import {
  4. PublicKey,
  5. Keypair,
  6. SystemProgram,
  7. sendAndConfirmTransaction,
  8. Transaction,
  9. AccountInfo,
  10. } from "@solana/web3.js";
  11. import {
  12. getExtraAccountMetaAddress,
  13. ExtraAccountMeta,
  14. getMintLen,
  15. ExtensionType,
  16. createInitializeTransferHookInstruction,
  17. createInitializeMintInstruction,
  18. createAssociatedTokenAccountInstruction,
  19. getAssociatedTokenAddressSync,
  20. createMintToInstruction,
  21. createTransferCheckedInstruction,
  22. getAccount,
  23. addExtraAccountsToInstruction,
  24. } from "@solana/spl-token";
  25. import { assert } from "chai";
  26. import { TransferHook } from "../target/types/transfer_hook";
  27. describe("transfer hook", () => {
  28. const provider = anchor.AnchorProvider.env();
  29. anchor.setProvider(provider);
  30. const TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(
  31. "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
  32. );
  33. const program = anchor.workspace.TransferHook as Program<TransferHook>;
  34. const decimals = 2;
  35. const mintAmount = 100;
  36. const transferAmount = 10;
  37. const payer = Keypair.generate();
  38. const mintAuthority = Keypair.generate();
  39. const mint = Keypair.generate();
  40. const sourceAuthority = Keypair.generate();
  41. const destinationAuthority = Keypair.generate().publicKey;
  42. let source: PublicKey = null;
  43. let destination: PublicKey = null;
  44. let extraMetasAddress: PublicKey = null;
  45. const validationLen = 8 + 4 + 4 + 2 * 35; // Discriminator, length, pod slice length, pod slice with 2 extra metas
  46. const extraMetas: ExtraAccountMeta[] = [
  47. {
  48. discriminator: 0,
  49. addressConfig: Keypair.generate().publicKey.toBuffer(),
  50. isWritable: false,
  51. isSigner: false,
  52. },
  53. {
  54. discriminator: 0,
  55. addressConfig: Keypair.generate().publicKey.toBuffer(),
  56. isWritable: false,
  57. isSigner: false,
  58. },
  59. ];
  60. before(async () => {
  61. const { programId } = program;
  62. const extensions = [ExtensionType.TransferHook];
  63. const mintLen = getMintLen(extensions);
  64. const lamports =
  65. await provider.connection.getMinimumBalanceForRentExemption(mintLen);
  66. source = getAssociatedTokenAddressSync(
  67. mint.publicKey,
  68. sourceAuthority.publicKey,
  69. false,
  70. TOKEN_2022_PROGRAM_ID
  71. );
  72. destination = getAssociatedTokenAddressSync(
  73. mint.publicKey,
  74. destinationAuthority,
  75. false,
  76. TOKEN_2022_PROGRAM_ID
  77. );
  78. extraMetasAddress = getExtraAccountMetaAddress(mint.publicKey, programId);
  79. const transaction = new Transaction().add(
  80. SystemProgram.createAccount({
  81. fromPubkey: payer.publicKey,
  82. newAccountPubkey: mint.publicKey,
  83. space: mintLen,
  84. lamports,
  85. programId: TOKEN_2022_PROGRAM_ID,
  86. }),
  87. createInitializeTransferHookInstruction(
  88. mint.publicKey,
  89. mintAuthority.publicKey,
  90. programId,
  91. TOKEN_2022_PROGRAM_ID
  92. ),
  93. createInitializeMintInstruction(
  94. mint.publicKey,
  95. decimals,
  96. mintAuthority.publicKey,
  97. mintAuthority.publicKey,
  98. TOKEN_2022_PROGRAM_ID
  99. ),
  100. createAssociatedTokenAccountInstruction(
  101. payer.publicKey,
  102. source,
  103. sourceAuthority.publicKey,
  104. mint.publicKey,
  105. TOKEN_2022_PROGRAM_ID
  106. ),
  107. createAssociatedTokenAccountInstruction(
  108. payer.publicKey,
  109. destination,
  110. destinationAuthority,
  111. mint.publicKey,
  112. TOKEN_2022_PROGRAM_ID
  113. ),
  114. createMintToInstruction(
  115. mint.publicKey,
  116. source,
  117. mintAuthority.publicKey,
  118. mintAmount,
  119. [],
  120. TOKEN_2022_PROGRAM_ID
  121. )
  122. );
  123. await provider.connection.confirmTransaction(
  124. await provider.connection.requestAirdrop(payer.publicKey, 10000000000),
  125. "confirmed"
  126. );
  127. await sendAndConfirmTransaction(provider.connection, transaction, [
  128. payer,
  129. mint,
  130. mintAuthority,
  131. ]);
  132. });
  133. it("can create an `InitializeExtraAccountMetaList` instruction with the proper discriminator", async () => {
  134. const ix = await program.methods
  135. .initialize(extraMetas as any[])
  136. .accounts({
  137. extraMetasAccount: extraMetasAddress,
  138. mint: mint.publicKey,
  139. mintAuthority: mintAuthority.publicKey,
  140. systemProgram: SystemProgram.programId,
  141. })
  142. .instruction();
  143. assert.equal(
  144. ix.data.subarray(0, 8).compare(
  145. Buffer.from([43, 34, 13, 49, 167, 88, 235, 235]) // SPL discriminator for `InitializeExtraAccountMetaList` from interface
  146. ),
  147. 0
  148. );
  149. const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode(
  150. ix.data,
  151. "hex"
  152. );
  153. assert.equal(name, "initialize");
  154. assert.property(data, "metas");
  155. assert.isArray(data.metas);
  156. assert.equal(data.metas.length, extraMetas.length);
  157. });
  158. it("can create an `Execute` instruction with the proper discriminator", async () => {
  159. const ix = await program.methods
  160. .execute(new anchor.BN(transferAmount))
  161. .accounts({
  162. sourceAccount: source,
  163. mint: mint.publicKey,
  164. destinationAccount: destination,
  165. ownerDelegate: sourceAuthority.publicKey,
  166. extraMetasAccount: extraMetasAddress,
  167. secondaryAuthority1: new PublicKey(extraMetas[0].addressConfig),
  168. secondaryAuthority2: new PublicKey(extraMetas[1].addressConfig),
  169. })
  170. .instruction();
  171. assert.equal(
  172. ix.data.subarray(0, 8).compare(
  173. Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]) // SPL discriminator for `Execute` from interface
  174. ),
  175. 0
  176. );
  177. const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode(
  178. ix.data,
  179. "hex"
  180. );
  181. assert.equal(name, "execute");
  182. assert.property(data, "amount");
  183. assert.isTrue(anchor.BN.isBN(data.amount));
  184. assert.isTrue(data.amount.eq(new anchor.BN(transferAmount)));
  185. });
  186. it("can transfer with extra account metas", async () => {
  187. // Initialize the extra metas
  188. await program.methods
  189. .initialize(extraMetas as any[])
  190. .accounts({
  191. extraMetasAccount: extraMetasAddress,
  192. mint: mint.publicKey,
  193. mintAuthority: mintAuthority.publicKey,
  194. systemProgram: SystemProgram.programId,
  195. })
  196. .signers([mintAuthority])
  197. .rpc();
  198. // Check the account data
  199. await provider.connection
  200. .getAccountInfo(extraMetasAddress)
  201. .then((account: AccountInfo<Buffer>) => {
  202. assert.equal(account.data.length, validationLen);
  203. assert.equal(
  204. account.data.subarray(0, 8).compare(
  205. Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]) // SPL discriminator for `Execute` from interface
  206. ),
  207. 0
  208. );
  209. assert.equal(
  210. account.data.subarray(8, 12).compare(
  211. Buffer.from([74, 0, 0, 0]) // Little endian 74
  212. ),
  213. 0
  214. );
  215. assert.equal(
  216. account.data.subarray(12, 16).compare(
  217. Buffer.from([2, 0, 0, 0]) // Little endian 2
  218. ),
  219. 0
  220. );
  221. const extraMetaToBuffer = (extraMeta: ExtraAccountMeta) => {
  222. const buf = Buffer.alloc(35);
  223. buf.set(extraMeta.addressConfig, 1);
  224. buf.writeUInt8(0, 33); // isSigner
  225. buf.writeUInt8(0, 34); // isWritable
  226. return buf;
  227. };
  228. assert.equal(
  229. account.data
  230. .subarray(16, 51)
  231. .compare(extraMetaToBuffer(extraMetas[0])),
  232. 0
  233. );
  234. assert.equal(
  235. account.data
  236. .subarray(51, 86)
  237. .compare(extraMetaToBuffer(extraMetas[1])),
  238. 0
  239. );
  240. });
  241. const ix = await addExtraAccountsToInstruction(
  242. provider.connection,
  243. createTransferCheckedInstruction(
  244. source,
  245. mint.publicKey,
  246. destination,
  247. sourceAuthority.publicKey,
  248. transferAmount,
  249. decimals,
  250. undefined,
  251. TOKEN_2022_PROGRAM_ID
  252. ),
  253. mint.publicKey,
  254. undefined,
  255. TOKEN_2022_PROGRAM_ID
  256. );
  257. await sendAndConfirmTransaction(
  258. provider.connection,
  259. new Transaction().add(ix),
  260. [payer, sourceAuthority]
  261. );
  262. // Check the resulting token balances
  263. await getAccount(
  264. provider.connection,
  265. source,
  266. undefined,
  267. TOKEN_2022_PROGRAM_ID
  268. ).then((account) => {
  269. assert.equal(account.amount, BigInt(mintAmount - transferAmount));
  270. });
  271. await getAccount(
  272. provider.connection,
  273. destination,
  274. undefined,
  275. TOKEN_2022_PROGRAM_ID
  276. ).then((account) => {
  277. assert.equal(account.amount, BigInt(transferAmount));
  278. });
  279. });
  280. });