escrow.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { randomBytes } from "node:crypto";
  2. import * as anchor from "@coral-xyz/anchor";
  3. import { BN, type Program } from "@coral-xyz/anchor";
  4. import {
  5. MINT_SIZE,
  6. TOKEN_2022_PROGRAM_ID,
  7. type TOKEN_PROGRAM_ID,
  8. createAssociatedTokenAccountIdempotentInstruction,
  9. createInitializeMint2Instruction,
  10. createMintToInstruction,
  11. getAssociatedTokenAddressSync,
  12. getMinimumBalanceForRentExemptMint,
  13. } from "@solana/spl-token";
  14. import {
  15. LAMPORTS_PER_SOL,
  16. PublicKey,
  17. SystemProgram,
  18. Transaction,
  19. type TransactionInstruction,
  20. } from "@solana/web3.js";
  21. import { assert } from "chai";
  22. import type { Escrow } from "../target/types/escrow";
  23. import { confirmTransaction, makeKeypairs } from "@solana-developers/helpers";
  24. const TOKEN_PROGRAM: typeof TOKEN_2022_PROGRAM_ID | typeof TOKEN_PROGRAM_ID =
  25. TOKEN_2022_PROGRAM_ID;
  26. const getRandomBigNumber = (size = 8) => {
  27. return new BN(randomBytes(size));
  28. };
  29. describe("escrow", async () => {
  30. anchor.setProvider(anchor.AnchorProvider.env());
  31. const provider = anchor.getProvider();
  32. const connection = provider.connection;
  33. const program = anchor.workspace.Escrow as Program<Escrow>;
  34. // We're going to reuse these accounts across multiple tests
  35. const accounts: Record<string, PublicKey> = {
  36. tokenProgram: TOKEN_PROGRAM,
  37. };
  38. const [alice, bob, tokenMintA, tokenMintB] = makeKeypairs(4);
  39. before(
  40. "Creates Alice and Bob accounts, 2 token mints, and associated token accounts for both tokens for both users",
  41. async () => {
  42. const [
  43. aliceTokenAccountA,
  44. aliceTokenAccountB,
  45. bobTokenAccountA,
  46. bobTokenAccountB,
  47. ] = [alice, bob].flatMap((keypair) =>
  48. [tokenMintA, tokenMintB].map((mint) =>
  49. getAssociatedTokenAddressSync(
  50. mint.publicKey,
  51. keypair.publicKey,
  52. false,
  53. TOKEN_PROGRAM
  54. )
  55. )
  56. );
  57. // Airdrops to users, and creates two tokens mints 'A' and 'B'"
  58. const minimumLamports = await getMinimumBalanceForRentExemptMint(
  59. connection
  60. );
  61. const sendSolInstructions: Array<TransactionInstruction> = [
  62. alice,
  63. bob,
  64. ].map((account) =>
  65. SystemProgram.transfer({
  66. fromPubkey: provider.publicKey,
  67. toPubkey: account.publicKey,
  68. lamports: 10 * LAMPORTS_PER_SOL,
  69. })
  70. );
  71. const createMintInstructions: Array<TransactionInstruction> = [
  72. tokenMintA,
  73. tokenMintB,
  74. ].map((mint) =>
  75. SystemProgram.createAccount({
  76. fromPubkey: provider.publicKey,
  77. newAccountPubkey: mint.publicKey,
  78. lamports: minimumLamports,
  79. space: MINT_SIZE,
  80. programId: TOKEN_PROGRAM,
  81. })
  82. );
  83. // Make tokenA and tokenB mints, mint tokens and create ATAs
  84. const mintTokensInstructions: Array<TransactionInstruction> = [
  85. {
  86. mint: tokenMintA.publicKey,
  87. authority: alice.publicKey,
  88. ata: aliceTokenAccountA,
  89. },
  90. {
  91. mint: tokenMintB.publicKey,
  92. authority: bob.publicKey,
  93. ata: bobTokenAccountB,
  94. },
  95. ].flatMap((mintDetails) => [
  96. createInitializeMint2Instruction(
  97. mintDetails.mint,
  98. 6,
  99. mintDetails.authority,
  100. null,
  101. TOKEN_PROGRAM
  102. ),
  103. createAssociatedTokenAccountIdempotentInstruction(
  104. provider.publicKey,
  105. mintDetails.ata,
  106. mintDetails.authority,
  107. mintDetails.mint,
  108. TOKEN_PROGRAM
  109. ),
  110. createMintToInstruction(
  111. mintDetails.mint,
  112. mintDetails.ata,
  113. mintDetails.authority,
  114. 1_000_000_000,
  115. [],
  116. TOKEN_PROGRAM
  117. ),
  118. ]);
  119. // Add all these instructions to our transaction
  120. const tx = new Transaction();
  121. tx.instructions = [
  122. ...sendSolInstructions,
  123. ...createMintInstructions,
  124. ...mintTokensInstructions,
  125. ];
  126. await provider.sendAndConfirm(tx, [tokenMintA, tokenMintB, alice, bob]);
  127. // Save the accounts for later use
  128. accounts.maker = alice.publicKey;
  129. accounts.taker = bob.publicKey;
  130. accounts.tokenMintA = tokenMintA.publicKey;
  131. accounts.makerTokenAccountA = aliceTokenAccountA;
  132. accounts.takerTokenAccountA = bobTokenAccountA;
  133. accounts.tokenMintB = tokenMintB.publicKey;
  134. accounts.makerTokenAccountB = aliceTokenAccountB;
  135. accounts.takerTokenAccountB = bobTokenAccountB;
  136. }
  137. );
  138. const tokenAOfferedAmount = new BN(1_000_000);
  139. const tokenBWantedAmount = new BN(1_000_000);
  140. // We'll call this function from multiple tests, so let's seperate it out
  141. const make = async () => {
  142. // Pick a random ID for the offer we'll make
  143. const offerId = getRandomBigNumber();
  144. // Then determine the account addresses we'll use for the offer and the vault
  145. const offer = PublicKey.findProgramAddressSync(
  146. [
  147. Buffer.from("offer"),
  148. accounts.maker.toBuffer(),
  149. offerId.toArrayLike(Buffer, "le", 8),
  150. ],
  151. program.programId
  152. )[0];
  153. const vault = getAssociatedTokenAddressSync(
  154. accounts.tokenMintA,
  155. offer,
  156. true,
  157. TOKEN_PROGRAM
  158. );
  159. accounts.offer = offer;
  160. accounts.vault = vault;
  161. const transactionSignature = await program.methods
  162. .makeOffer(offerId, tokenAOfferedAmount, tokenBWantedAmount)
  163. .accounts({ ...accounts })
  164. .signers([alice])
  165. .rpc();
  166. await confirmTransaction(connection, transactionSignature);
  167. // Check our vault contains the tokens offered
  168. const vaultBalanceResponse = await connection.getTokenAccountBalance(vault);
  169. const vaultBalance = new BN(vaultBalanceResponse.value.amount);
  170. assert(vaultBalance.eq(tokenAOfferedAmount));
  171. // Check our Offer account contains the correct data
  172. const offerAccount = await program.account.offer.fetch(offer);
  173. assert(offerAccount.maker.equals(alice.publicKey));
  174. assert(offerAccount.tokenMintA.equals(accounts.tokenMintA));
  175. assert(offerAccount.tokenMintB.equals(accounts.tokenMintB));
  176. assert(offerAccount.tokenBWantedAmount.eq(tokenBWantedAmount));
  177. };
  178. // We'll call this function from multiple tests, so let's seperate it out
  179. const take = async () => {
  180. const transactionSignature = await program.methods
  181. .takeOffer()
  182. .accounts({ ...accounts })
  183. .signers([bob])
  184. .rpc();
  185. await confirmTransaction(connection, transactionSignature);
  186. // Check the offered tokens are now in Bob's account
  187. // (note: there is no before balance as Bob didn't have any offered tokens before the transaction)
  188. const bobTokenAccountBalanceAfterResponse =
  189. await connection.getTokenAccountBalance(accounts.takerTokenAccountA);
  190. const bobTokenAccountBalanceAfter = new BN(
  191. bobTokenAccountBalanceAfterResponse.value.amount
  192. );
  193. assert(bobTokenAccountBalanceAfter.eq(tokenAOfferedAmount));
  194. // Check the wanted tokens are now in Alice's account
  195. // (note: there is no before balance as Alice didn't have any wanted tokens before the transaction)
  196. const aliceTokenAccountBalanceAfterResponse =
  197. await connection.getTokenAccountBalance(accounts.makerTokenAccountB);
  198. const aliceTokenAccountBalanceAfter = new BN(
  199. aliceTokenAccountBalanceAfterResponse.value.amount
  200. );
  201. assert(aliceTokenAccountBalanceAfter.eq(tokenBWantedAmount));
  202. };
  203. it("Puts the tokens Alice offers into the vault when Alice makes an offer", async () => {
  204. await make();
  205. });
  206. it("Puts the tokens from the vault into Bob's account, and gives Alice Bob's tokens, when Bob takes an offer", async () => {
  207. await take();
  208. });
  209. });