import { randomBytes } from 'node:crypto'; import { describe, it } from 'node:test'; 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 { BankrunProvider } from 'anchor-bankrun'; import { assert } from 'chai'; import { startAnchor } from 'solana-bankrun'; import type { Escrow } from '../target/types/escrow'; import { confirmTransaction, makeKeypairs } from '@solana-developers/helpers'; const TOKEN_PROGRAM: typeof TOKEN_2022_PROGRAM_ID | typeof TOKEN_PROGRAM_ID = TOKEN_2022_PROGRAM_ID; const IDL = require('../target/idl/escrow.json'); const PROGRAM_ID = new PublicKey(IDL.address); const getRandomBigNumber = (size = 8) => { return new BN(randomBytes(size)); }; describe('Escrow Bankrun example', async () => { const context = await startAnchor('', [{ name: 'escrow', programId: PROGRAM_ID }], []); const provider = new BankrunProvider(context); const connection = provider.connection; // const payer = provider.wallet as anchor.Wallet; const program = new anchor.Program(IDL, provider); // We're going to reuse these accounts across multiple tests const accounts: Record = { tokenProgram: TOKEN_PROGRAM, }; const [alice, bob, tokenMintA, tokenMintB] = makeKeypairs(4); 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)), ); // Airdrops to users, and creates two tokens mints 'A' and 'B'" const minimumLamports = await getMinimumBalanceForRentExemptMint(connection); const sendSolInstructions: Array = [alice, bob].map((account) => SystemProgram.transfer({ fromPubkey: provider.publicKey, toPubkey: account.publicKey, lamports: 10 * LAMPORTS_PER_SOL, }), ); const createMintInstructions: Array = [tokenMintA, tokenMintB].map((mint) => SystemProgram.createAccount({ fromPubkey: provider.publicKey, newAccountPubkey: mint.publicKey, lamports: minimumLamports, space: MINT_SIZE, programId: TOKEN_PROGRAM, }), ); // Make tokenA and tokenB mints, mint tokens and create ATAs const mintTokensInstructions: Array = [ { 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]); // Save the accounts for later use accounts.maker = alice.publicKey; accounts.taker = bob.publicKey; accounts.tokenMintA = tokenMintA.publicKey; accounts.makerTokenAccountA = aliceTokenAccountA; accounts.takerTokenAccountA = bobTokenAccountA; accounts.tokenMintB = tokenMintB.publicKey; accounts.makerTokenAccountB = aliceTokenAccountB; 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 () => { // Pick a random ID for the offer we'll make const offerId = getRandomBigNumber(); // Then determine the account addresses we'll use for the offer and the vault const offer = PublicKey.findProgramAddressSync( [Buffer.from('offer'), accounts.maker.toBuffer(), offerId.toArrayLike(Buffer, 'le', 8)], program.programId, )[0]; const vault = getAssociatedTokenAddressSync(accounts.tokenMintA, offer, true, TOKEN_PROGRAM); accounts.offer = offer; accounts.vault = vault; const transactionSignature = await program.methods .makeOffer(offerId, tokenAOfferedAmount, tokenBWantedAmount) .accounts({ ...accounts }) .signers([alice]) .rpc(); await confirmTransaction(connection, transactionSignature); // Check our vault contains the tokens offered const vaultBalanceResponse = await connection.getTokenAccountBalance(vault); const vaultBalance = new BN(vaultBalanceResponse.value.amount); assert(vaultBalance.eq(tokenAOfferedAmount)); // Check our Offer account contains the correct data const offerAccount = await program.account.offer.fetch(offer); assert(offerAccount.maker.equals(alice.publicKey)); assert(offerAccount.tokenMintA.equals(accounts.tokenMintA)); assert(offerAccount.tokenMintB.equals(accounts.tokenMintB)); assert(offerAccount.tokenBWantedAmount.eq(tokenBWantedAmount)); }; // We'll call this function from multiple tests, so let's seperate it out const take = async () => { const transactionSignature = await program.methods .takeOffer() .accounts({ ...accounts }) .signers([bob]) .rpc(); await confirmTransaction(connection, transactionSignature); // Check the offered tokens are now in Bob's account // (note: there is no before balance as Bob didn't have any offered tokens before the transaction) const bobTokenAccountBalanceAfterResponse = await connection.getTokenAccountBalance(accounts.takerTokenAccountA); const bobTokenAccountBalanceAfter = new BN(bobTokenAccountBalanceAfterResponse.value.amount); assert(bobTokenAccountBalanceAfter.eq(tokenAOfferedAmount)); // Check the wanted tokens are now in Alice's account // (note: there is no before balance as Alice didn't have any wanted tokens before the transaction) 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(); }); });