Browse Source

Use new helper to simplify escrow tests (#108)

* Use new helper to make Escrow tests faster

* Update pnpm lock

* Add transfer_tokens() to replace copypasted code

* Split up sign_seeds to be less ugly

* Avoid premature optimisation and just move make and take into their respective tests

* Format, no changes

* Make a variable for mocha slow test threshhold

* Make less ugly

* Neaten

* Log signature

* add comments in test to provide context for accounts

---------

Co-authored-by: John <75003086+ZYJLiu@users.noreply.github.com>
Mike MacCana 1 year ago
parent
commit
b992087cd6

+ 3 - 2
basics/favorites/anchor/programs/favorites/src/lib.rs

@@ -13,8 +13,8 @@ pub mod favorites {
 
     // Our instruction handler! It sets the user's favorite number and color
     pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String, hobbies: Vec<String>) -> Result<()> {
-        let user_public_key = context.accounts.user.key();
         msg!("Greetings from {}", context.program_id);
+        let user_public_key = context.accounts.user.key();
         msg!(
             "User {user_public_key}'s favorite number is {number}, favorite color is: {color}, and their hobbies are {hobbies:?}",
         );
@@ -53,7 +53,8 @@ pub struct SetFavorites<'info> {
         payer = user, 
         space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE, 
         seeds=[b"favorites", user.key().as_ref()],
-    bump)]
+        bump
+    )]
     pub favorites: Account<'info, Favorites>,
 
     pub system_program: Program<'info, System>,

+ 3 - 1
basics/favorites/anchor/tests/favorites.ts

@@ -54,7 +54,9 @@ describe('Favorites', () => {
   it('Updates the favorites', async () => {
     const newFavoriteHobbies = ['skiing', 'skydiving', 'biking', 'swimming'];
     try {
-      await program.methods.setFavorites(favoriteNumber, favoriteColor, newFavoriteHobbies).signers([user]).rpc();
+      const signature = await program.methods.setFavorites(favoriteNumber, favoriteColor, newFavoriteHobbies).signers([user]).rpc();
+
+      console.log(`Transaction signature: ${signature}`);
     } catch (error) {
       console.error((error as Error).message);
       const customErrorMessage = getCustomErrorMessage(systemProgramErrors, error);

+ 1 - 0
tokens/escrow/anchor/README.md

@@ -25,6 +25,7 @@ This project is based on [Dean Little's Anchor Escrow,](https://github.com/deanm
 One of the challenges when teaching is avoiding ambiguity — names have to be carefully chosen to be clear and not possible to confuse with other times.
 
 - Custom instructions were replaced by `@solana-developers/helpers` for many tasks to reduce the file size.
+- Shared functionality to transfer tokens is now in `instructions/shared.rs`
 - The upstream project has a custom file layout. We use the 'multiple files' Anchor layout.
 - Contexts are separate data structures from functions that use the contexts. There is no need for OO-like `impl` patterns here - there's no mutable state stored in the Context, and the 'methods' do not mutate that state. Besides, it's easier to type!
 - The name 'deposit' was being used in multiple contexts, and `deposit` can be tough because it's a verb and a noun:

+ 1 - 1
tokens/escrow/anchor/package.json

@@ -5,7 +5,7 @@
   },
   "dependencies": {
     "@coral-xyz/anchor": "^0.30.0",
-    "@solana-developers/helpers": "^2.3.0",
+    "@solana-developers/helpers": "^2.4.0",
     "@solana/spl-token": "^0.4.6"
   },
   "license": "MIT",

File diff suppressed because it is too large
+ 244 - 617
tokens/escrow/anchor/pnpm-lock.yaml


+ 10 - 17
tokens/escrow/anchor/programs/escrow/src/instructions/make_offer.rs

@@ -2,11 +2,13 @@ use anchor_lang::prelude::*;
 
 use anchor_spl::{
     associated_token::AssociatedToken,
-    token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},
+    token_interface::{Mint, TokenAccount, TokenInterface},
 };
 
 use crate::{Offer, ANCHOR_DISCRIMINATOR};
 
+use super::transfer_tokens;
+
 // See https://www.anchor-lang.com/docs/account-constraints#instruction-attribute
 #[derive(Accounts)]
 #[instruction(id: u64)]
@@ -56,22 +58,13 @@ pub fn send_offered_tokens_to_vault(
     context: &Context<MakeOffer>,
     token_a_offered_amount: u64,
 ) -> Result<()> {
-    let transfer_accounts = TransferChecked {
-        from: context.accounts.maker_token_account_a.to_account_info(),
-        mint: context.accounts.token_mint_a.to_account_info(),
-        to: context.accounts.vault.to_account_info(),
-        authority: context.accounts.maker.to_account_info(),
-    };
-
-    let cpi_context = CpiContext::new(
-        context.accounts.token_program.to_account_info(),
-        transfer_accounts,
-    );
-
-    transfer_checked(
-        cpi_context,
-        token_a_offered_amount,
-        context.accounts.token_mint_a.decimals,
+    transfer_tokens(
+        &context.accounts.maker_token_account_a,
+        &context.accounts.vault,
+        &token_a_offered_amount,
+        &context.accounts.token_mint_a,
+        &context.accounts.maker,
+        &context.accounts.token_program,
     )
 }
 

+ 3 - 0
tokens/escrow/anchor/programs/escrow/src/instructions/mod.rs

@@ -3,3 +3,6 @@ pub use make_offer::*;
 
 pub mod take_offer;
 pub use take_offer::*;
+
+pub mod shared;
+pub use shared::*;

+ 25 - 0
tokens/escrow/anchor/programs/escrow/src/instructions/shared.rs

@@ -0,0 +1,25 @@
+use anchor_lang::prelude::*;
+
+use anchor_spl::token_interface::{
+    transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,
+};
+
+pub fn transfer_tokens<'info>(
+    from: &InterfaceAccount<'info, TokenAccount>,
+    to: &InterfaceAccount<'info, TokenAccount>,
+    amount: &u64,
+    mint: &InterfaceAccount<'info, Mint>,
+    authority: &Signer<'info>,
+    token_program: &Interface<'info, TokenInterface>,
+) -> Result<()> {
+    let transfer_accounts = TransferChecked {
+        from: from.to_account_info(),
+        mint: mint.to_account_info(),
+        to: to.to_account_info(),
+        authority: authority.to_account_info(),
+    };
+
+    let cpi_context = CpiContext::new(token_program.to_account_info(), transfer_accounts);
+
+    transfer_checked(cpi_context, *amount, mint.decimals)
+}

+ 12 - 18
tokens/escrow/anchor/programs/escrow/src/instructions/take_offer.rs

@@ -10,6 +10,8 @@ use anchor_spl::{
 
 use crate::Offer;
 
+use super::transfer_tokens;
+
 #[derive(Accounts)]
 pub struct TakeOffer<'info> {
     #[account(mut)]
@@ -73,32 +75,24 @@ pub struct TakeOffer<'info> {
 }
 
 pub fn send_wanted_tokens_to_maker(ctx: &Context<TakeOffer>) -> Result<()> {
-    let transfer_accounts = TransferChecked {
-        from: ctx.accounts.taker_token_account_b.to_account_info(),
-        mint: ctx.accounts.token_mint_b.to_account_info(),
-        to: ctx.accounts.maker_token_account_b.to_account_info(),
-        authority: ctx.accounts.taker.to_account_info(),
-    };
-
-    let cpi_ctx = CpiContext::new(
-        ctx.accounts.token_program.to_account_info(),
-        transfer_accounts,
-    );
-
-    transfer_checked(
-        cpi_ctx,
-        ctx.accounts.offer.token_b_wanted_amount,
-        ctx.accounts.token_mint_b.decimals,
+    transfer_tokens(
+        &ctx.accounts.taker_token_account_b,
+        &ctx.accounts.maker_token_account_b,
+        &ctx.accounts.offer.token_b_wanted_amount,
+        &ctx.accounts.token_mint_b,
+        &ctx.accounts.taker,
+        &ctx.accounts.token_program,
     )
 }
 
 pub fn withdraw_and_close_vault(ctx: Context<TakeOffer>) -> Result<()> {
-    let signer_seeds: [&[&[u8]]; 1] = [&[
+    let seeds = &[
         b"offer",
         ctx.accounts.maker.to_account_info().key.as_ref(),
         &ctx.accounts.offer.id.to_le_bytes()[..],
         &[ctx.accounts.offer.bump],
-    ]];
+    ];
+    let signer_seeds = [&seeds[..]];
 
     let accounts = TransferChecked {
         from: ctx.accounts.vault.to_account_info(),

+ 70 - 75
tokens/escrow/anchor/tests/escrow.ts

@@ -1,32 +1,35 @@
 import { randomBytes } from 'node:crypto';
 import * as anchor from '@coral-xyz/anchor';
 import { BN, type Program } from '@coral-xyz/anchor';
-import {
-  MINT_SIZE,
-  TOKEN_2022_PROGRAM_ID,
-  type TOKEN_PROGRAM_ID,
-  createAssociatedTokenAccountIdempotentInstruction,
-  createInitializeMint2Instruction,
-  createMintToInstruction,
-  getAssociatedTokenAddressSync,
-  getMinimumBalanceForRentExemptMint,
-} from '@solana/spl-token';
-import { LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, type TransactionInstruction } from '@solana/web3.js';
+import { TOKEN_2022_PROGRAM_ID, type TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token';
+import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
 import { assert } from 'chai';
 import type { Escrow } from '../target/types/escrow';
 
-import { confirmTransaction, makeKeypairs } from '@solana-developers/helpers';
+import { confirmTransaction, createAccountsMintsAndTokenAccounts, makeKeypairs } from '@solana-developers/helpers';
 
+// Work on both Token Program and new Token Extensions Program
 const TOKEN_PROGRAM: typeof TOKEN_2022_PROGRAM_ID | typeof TOKEN_PROGRAM_ID = TOKEN_2022_PROGRAM_ID;
 
+const SECONDS = 1000;
+
+// Tests must complete within half this time otherwise
+// they are marked as slow. Since Anchor involves a little
+// network IO, these tests usually take about 15 seconds.
+const ANCHOR_SLOW_TEST_THRESHOLD = 40 * SECONDS;
+
 const getRandomBigNumber = (size = 8) => {
   return new BN(randomBytes(size));
 };
 
 describe('escrow', async () => {
-  anchor.setProvider(anchor.AnchorProvider.env());
+  // Use the cluster and the keypair from Anchor.toml
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
 
-  const provider = anchor.getProvider();
+  // See https://github.com/coral-xyz/anchor/issues/3122
+  const user = (provider.wallet as anchor.Wallet).payer;
+  const payer = user;
 
   const connection = provider.connection;
 
@@ -37,57 +40,62 @@ describe('escrow', async () => {
     tokenProgram: TOKEN_PROGRAM,
   };
 
-  const [alice, bob, tokenMintA, tokenMintB] = makeKeypairs(4);
+  let alice: anchor.web3.Keypair;
+  let bob: anchor.web3.Keypair;
+  let tokenMintA: anchor.web3.Keypair;
+  let tokenMintB: anchor.web3.Keypair;
+
+  [alice, bob, tokenMintA, tokenMintB] = makeKeypairs(4);
+
+  const tokenAOfferedAmount = new BN(1_000_000);
+  const tokenBWantedAmount = new BN(1_000_000);
 
   before('Creates Alice and Bob accounts, 2 token mints, and associated token accounts for both tokens for both users', async () => {
-    const [aliceTokenAccountA, aliceTokenAccountB, bobTokenAccountA, bobTokenAccountB] = [alice, bob].flatMap((keypair) =>
-      [tokenMintA, tokenMintB].map((mint) => getAssociatedTokenAddressSync(mint.publicKey, keypair.publicKey, false, TOKEN_PROGRAM)),
+    const usersMintsAndTokenAccounts = await createAccountsMintsAndTokenAccounts(
+      [
+        // Alice's token balances
+        [
+          // 1_000_000_000 of token A
+          1_000_000_000,
+          // 0 of token B
+          0,
+        ],
+        // Bob's token balances
+        [
+          // 0 of token A
+          0,
+          // 1_000_000_000 of token B
+          1_000_000_000,
+        ],
+      ],
+      1 * LAMPORTS_PER_SOL,
+      connection,
+      payer,
     );
 
-    // Airdrops to users, and creates two tokens mints 'A' and 'B'"
-    const minimumLamports = await getMinimumBalanceForRentExemptMint(connection);
+    // Alice will be the maker (creator) of the offer
+    // Bob will be the taker (acceptor) of the offer
+    const users = usersMintsAndTokenAccounts.users;
+    alice = users[0];
+    bob = users[1];
 
-    const sendSolInstructions: Array<TransactionInstruction> = [alice, bob].map((account) =>
-      SystemProgram.transfer({
-        fromPubkey: provider.publicKey,
-        toPubkey: account.publicKey,
-        lamports: 10 * LAMPORTS_PER_SOL,
-      }),
-    );
+    // tokenMintA represents the token Alice is offering
+    // tokenMintB represents the token Alice wants in return
+    const mints = usersMintsAndTokenAccounts.mints;
+    tokenMintA = mints[0];
+    tokenMintB = mints[1];
 
-    const createMintInstructions: Array<TransactionInstruction> = [tokenMintA, tokenMintB].map((mint) =>
-      SystemProgram.createAccount({
-        fromPubkey: provider.publicKey,
-        newAccountPubkey: mint.publicKey,
-        lamports: minimumLamports,
-        space: MINT_SIZE,
-        programId: TOKEN_PROGRAM,
-      }),
-    );
+    const tokenAccounts = usersMintsAndTokenAccounts.tokenAccounts;
+
+    // aliceTokenAccountA is Alice's account for tokenA (the token she's offering)
+    // aliceTokenAccountB is Alice's account for tokenB (the token she wants)
+    const aliceTokenAccountA = tokenAccounts[0][0];
+    const aliceTokenAccountB = tokenAccounts[0][1];
 
-    // Make tokenA and tokenB mints, mint tokens and create ATAs
-    const mintTokensInstructions: Array<TransactionInstruction> = [
-      {
-        mint: tokenMintA.publicKey,
-        authority: alice.publicKey,
-        ata: aliceTokenAccountA,
-      },
-      {
-        mint: tokenMintB.publicKey,
-        authority: bob.publicKey,
-        ata: bobTokenAccountB,
-      },
-    ].flatMap((mintDetails) => [
-      createInitializeMint2Instruction(mintDetails.mint, 6, mintDetails.authority, null, TOKEN_PROGRAM),
-      createAssociatedTokenAccountIdempotentInstruction(provider.publicKey, mintDetails.ata, mintDetails.authority, mintDetails.mint, TOKEN_PROGRAM),
-      createMintToInstruction(mintDetails.mint, mintDetails.ata, mintDetails.authority, 1_000_000_000, [], TOKEN_PROGRAM),
-    ]);
-
-    // Add all these instructions to our transaction
-    const tx = new Transaction();
-    tx.instructions = [...sendSolInstructions, ...createMintInstructions, ...mintTokensInstructions];
-
-    await provider.sendAndConfirm(tx, [tokenMintA, tokenMintB, alice, bob]);
+    // bobTokenAccountA is Bob's account for tokenA (the token Alice is offering)
+    // bobTokenAccountB is Bob's account for tokenB (the token Alice wants)
+    const bobTokenAccountA = tokenAccounts[1][0];
+    const bobTokenAccountB = tokenAccounts[1][1];
 
     // Save the accounts for later use
     accounts.maker = alice.publicKey;
@@ -100,11 +108,7 @@ describe('escrow', async () => {
     accounts.takerTokenAccountB = bobTokenAccountB;
   });
 
-  const tokenAOfferedAmount = new BN(1_000_000);
-  const tokenBWantedAmount = new BN(1_000_000);
-
-  // We'll call this function from multiple tests, so let's seperate it out
-  const make = async () => {
+  it('Puts the tokens Alice offers into the vault when Alice makes an offer', async () => {
     // Pick a random ID for the offer we'll make
     const offerId = getRandomBigNumber();
 
@@ -139,10 +143,9 @@ describe('escrow', async () => {
     assert(offerAccount.tokenMintA.equals(accounts.tokenMintA));
     assert(offerAccount.tokenMintB.equals(accounts.tokenMintB));
     assert(offerAccount.tokenBWantedAmount.eq(tokenBWantedAmount));
-  };
+  }).slow(ANCHOR_SLOW_TEST_THRESHOLD);
 
-  // We'll call this function from multiple tests, so let's seperate it out
-  const take = async () => {
+  it("Puts the tokens from the vault into Bob's account, and gives Alice Bob's tokens, when Bob takes an offer", async () => {
     const transactionSignature = await program.methods
       .takeOffer()
       .accounts({ ...accounts })
@@ -162,13 +165,5 @@ describe('escrow', async () => {
     const aliceTokenAccountBalanceAfterResponse = await connection.getTokenAccountBalance(accounts.makerTokenAccountB);
     const aliceTokenAccountBalanceAfter = new BN(aliceTokenAccountBalanceAfterResponse.value.amount);
     assert(aliceTokenAccountBalanceAfter.eq(tokenBWantedAmount));
-  };
-
-  it('Puts the tokens Alice offers into the vault when Alice makes an offer', async () => {
-    await make();
-  });
-
-  it("Puts the tokens from the vault into Bob's account, and gives Alice Bob's tokens, when Bob takes an offer", async () => {
-    await take();
-  });
+  }).slow(ANCHOR_SLOW_TEST_THRESHOLD);
 });

Some files were not shown because too many files changed in this diff