escrow.ts 6.9 KB

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