Contract #1

use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    entrypoint,
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
    msg,
    rent::Rent,
    sysvar::Sysvar,
    system_instruction,
    program::invoke,
};

#[derive(BorshSerialize, BorshDeserialize)]
struct CounterState {
    count: u32,
}

#[derive(BorshSerialize, BorshDeserialize)]
enum CounterInstruction {
    Initialize,
    Double,
    Half,
}

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = CounterInstruction::try_from_slice(instruction_data)
        .map_err(|_| ProgramError::InvalidInstructionData)?;

    match instruction {
        CounterInstruction::Initialize => {
            msg!("Initializing counter");
            
            let mut iter = accounts.iter();
            let data_account = next_account_info(&mut iter)?;
            let payer = next_account_info(&mut iter)?;
            let system_program = next_account_info(&mut iter)?;
            
            // Check if payer is signer
            if !payer.is_signer {
                return Err(ProgramError::MissingRequiredSignature);
            }
            
            // Calculate space needed for CounterState
            let space = 4;
            
            // Calculate rent exemption amount
            let rent = Rent::get()?;
            let lamports = rent.minimum_balance(space);
            
            // Create the account
            let create_account_ix = system_instruction::create_account(
                payer.key,
                data_account.key,
                lamports,
                space as u64,
                program_id,
            );
            
            invoke(
                &create_account_ix,
                &[
                    payer.clone(),
                    data_account.clone(),
                    system_program.clone(),
                ],
            )?;
            
            // Initialize the account data
            let counter_state = CounterState { count: 1 };
            counter_state.serialize(&mut *data_account.data.borrow_mut())?;
        }
        CounterInstruction::Double => {
            msg!("Doubling counter");
            
            let mut iter = accounts.iter();
            let data_account = next_account_info(&mut iter)?;
            
            // Check if the account is owned by this program
            if data_account.owner != program_id {
                return Err(ProgramError::IncorrectProgramId);
            }
            
            let mut counter_state = CounterState::try_from_slice(&data_account.data.borrow())?;
            counter_state.count = counter_state.count * 2;
            counter_state.serialize(&mut *data_account.data.borrow_mut())?;
        }
        CounterInstruction::Half => {
            msg!("Halving counter");
            
            let mut iter = accounts.iter();
            let data_account = next_account_info(&mut iter)?;
            
            // Check if the account is owned by this program
            if data_account.owner != program_id {
                return Err(ProgramError::IncorrectProgramId);
            }
            
            let mut counter_state = CounterState::try_from_slice(&data_account.data.borrow())?;
            counter_state.count = counter_state.count / 2;
            counter_state.serialize(&mut *data_account.data.borrow_mut())?;
        }
    }
    
    Ok(())
}

Tests

import * as path from "path";
import {
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction
} from "@solana/web3.js";
import { LiteSVM } from "litesvm";
import { expect, test, describe, beforeAll } from "bun:test";
import { deserialize } from "borsh";
import * as borsh from "borsh";

describe("Counter Program Tests", () => {
  let svm: LiteSVM;
  let programId: PublicKey;
  let dataAccount: Keypair;
  let userAccount: Keypair;

  const programPath = path.join(import.meta.dir, "double.so");

  beforeAll(() => {
    svm = new LiteSVM();
    
    programId = PublicKey.unique();
    
    svm.addProgramFromFile(programId, programPath);
    
    dataAccount = new Keypair();
    
    userAccount = new Keypair();

    svm.airdrop(userAccount.publicKey, BigInt(LAMPORTS_PER_SOL));
  });

  test("initialize counter", () => {
    const instruction = new TransactionInstruction({
      programId,
      keys: [
        { pubkey: dataAccount.publicKey, isSigner: true, isWritable: true },
        { pubkey: userAccount.publicKey, isSigner: true, isWritable: true },
        { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
      ],
      data: Buffer.from([0])
    });

    const transaction = new Transaction().add(instruction);
    transaction.recentBlockhash = svm.latestBlockhash();
    transaction.feePayer = userAccount.publicKey;
    transaction.sign(dataAccount, userAccount);
    let txn = svm.sendTransaction(transaction);
    svm.expireBlockhash();
    const updatedAccountData = svm.getAccount(dataAccount.publicKey);
    if (!updatedAccountData) {
      throw new Error("Account not found");
    }

    expect(updatedAccountData.data[0]).toBe(1);
    expect(updatedAccountData.data[1]).toBe(0);
    expect(updatedAccountData.data[2]).toBe(0);
    expect(updatedAccountData.data[3]).toBe(0);
  });

  test("double counter value makes it 16 after 4 times", async () => {

    function doubleCounter() {
      // Create an instruction to call our program
      const instruction = new TransactionInstruction({
        programId,
        keys: [
          { pubkey: dataAccount.publicKey, isSigner: true, isWritable: true }
        ],
        data: Buffer.from([1])
      });
      
      // Create and execute the transaction
      let transaction = new Transaction().add(instruction);
      transaction.recentBlockhash = svm.latestBlockhash();

      transaction.feePayer = userAccount.publicKey;
      transaction.sign(userAccount, dataAccount);
      svm.sendTransaction(transaction);
      svm.expireBlockhash();
    }

    doubleCounter();
    doubleCounter();
    doubleCounter();
    doubleCounter();
    
    const updatedAccountData = svm.getAccount(dataAccount.publicKey);
    if (!updatedAccountData) {
      throw new Error("Account not found");
    }

    expect(updatedAccountData.data[0]).toBe(16);
    expect(updatedAccountData.data[1]).toBe(0);
    expect(updatedAccountData.data[2]).toBe(0);
    expect(updatedAccountData.data[3]).toBe(0);
  });
});