Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mollusk Tests

Pre-Alpha Disclaimer: This is an early pre-alpha release for exploring the SDK and starting development only. There is no real MPC signing — all signatures are generated by a single mock signer, not a distributed network. Do not submit any real transactions for signing or rely on any security guarantees. The dWallet keys, trust model, and signing protocol are not final; do not rely on any key material until mainnet. All interfaces, APIs, and data formats are subject to change without notice. The Solana program and all on-chain data will be wiped periodically and everything will be deleted when we transition to Ika Alpha 1. This software is provided “as is” without warranty of any kind; use is entirely at your own risk and dWallet Labs assumes no liability for any damages arising from its use.

Overview

Mollusk is the fastest way to test individual instructions in isolation. It runs a single instruction against pre-built account state – no validator, no network, no startup cost.

Mollusk is best for:

  • Verifying instruction data parsing
  • Checking signer and account validation
  • Testing discriminator handling
  • Validating PDA creation and field writes
  • Testing error conditions (double votes, closed proposals, missing signers)

Mollusk cannot test CPI calls (e.g., quorum triggering approve_message), because it runs a single program in isolation.

Setup

[dev-dependencies]
mollusk-svm = "0.2"
solana-account = "2"
solana-instruction = "2"
solana-pubkey = "2"
#![allow(unused)]
fn main() {
use mollusk_svm::Mollusk;
use solana_account::Account;
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::Pubkey;

const PROGRAM_PATH: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../target/deploy/ika_example_voting"
);

fn setup() -> (Mollusk, Pubkey) {
    let program_id = Pubkey::new_unique();
    let mollusk = Mollusk::new(&program_id, PROGRAM_PATH);
    (mollusk, program_id)
}
}

Account Helpers

Pre-build account state for test inputs:

#![allow(unused)]
fn main() {
fn funded_account() -> Account {
    Account {
        lamports: 10_000_000_000,
        data: vec![],
        owner: SYSTEM_PROGRAM_ID,
        executable: false,
        rent_epoch: 0,
    }
}

fn program_account(owner: &Pubkey, data: Vec<u8>) -> Account {
    Account {
        lamports: ((data.len() as u64 + 128) * 6960).max(1),
        data,
        owner: *owner,
        executable: false,
        rent_epoch: 0,
    }
}

fn empty_account() -> Account {
    Account {
        lamports: 0,
        data: vec![],
        owner: SYSTEM_PROGRAM_ID,
        executable: false,
        rent_epoch: 0,
    }
}
}

Writing a Test

1. Build the Instruction

#![allow(unused)]
fn main() {
fn build_create_proposal_ix(
    program_id: &Pubkey,
    proposal: &Pubkey,
    dwallet: &Pubkey,
    creator: &Pubkey,
    payer: &Pubkey,
    proposal_id: [u8; 32],
    message_hash: [u8; 32],
    quorum: u32,
    bump: u8,
) -> Instruction {
    let mut ix_data = Vec::with_capacity(104);
    ix_data.push(0); // discriminator
    ix_data.extend_from_slice(&proposal_id);
    ix_data.extend_from_slice(&message_hash);
    ix_data.extend_from_slice(&[0u8; 32]); // user_pubkey
    ix_data.push(0); // signature_scheme
    ix_data.extend_from_slice(&quorum.to_le_bytes());
    ix_data.push(0); // message_approval_bump
    ix_data.push(bump);

    Instruction {
        program_id: *program_id,
        accounts: vec![
            AccountMeta::new(*proposal, false),
            AccountMeta::new_readonly(*dwallet, false),
            AccountMeta::new_readonly(*creator, true),
            AccountMeta::new(*payer, true),
            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
        ],
        data: ix_data,
    }
}
}

2. Process and Assert

#![allow(unused)]
fn main() {
#[test]
fn test_create_proposal_success() {
    let (mollusk, program_id) = setup();
    let creator = Pubkey::new_unique();
    let payer = Pubkey::new_unique();
    let proposal_id = [0x01u8; 32];

    let (proposal_pda, bump) =
        Pubkey::find_program_address(&[b"proposal", &proposal_id], &program_id);

    let ix = build_create_proposal_ix(
        &program_id, &proposal_pda, &Pubkey::new_unique(),
        &creator, &payer, proposal_id, [0x42u8; 32], 3, bump,
    );

    let result = mollusk.process_instruction(
        &ix,
        &[
            (proposal_pda, empty_account()),
            (Pubkey::new_unique(), funded_account()),
            (creator, funded_account()),
            (payer, funded_account()),
            (SYSTEM_PROGRAM_ID, system_program_account()),
        ],
    );

    assert!(result.program_result.is_ok());

    let prop_data = &result.resulting_accounts[0].1.data;
    assert_eq!(prop_data[0], 1); // discriminator
    assert_eq!(prop_data[1], 1); // version
}
}

Test Patterns

Verify Error Conditions

#![allow(unused)]
fn main() {
#[test]
fn test_double_vote_fails() {
    let (mollusk, program_id) = setup();
    // Pre-populate VoteRecord (voter already voted)
    let existing_vr = build_vote_record_data(&voter, &proposal_id, 1, vr_bump);

    let result = mollusk.process_instruction(
        &ix,
        &[
            (proposal_pda, program_account(&program_id, proposal_data)),
            (vote_record_pda, program_account(&program_id, existing_vr)),
            // ...
        ],
    );

    assert!(result.program_result.is_err());
}
}

Verify Field Values

#![allow(unused)]
fn main() {
let prop_data = &result.resulting_accounts[0].1.data;
assert_eq!(read_u32(prop_data, 163), 1, "yes_votes = 1");
assert_eq!(read_u32(prop_data, 167), 0, "no_votes = 0");
assert_eq!(prop_data[175], 0, "status = Open");
}

Running Mollusk Tests

cargo test -p ika-example-voting

Tests run in milliseconds – no validator startup required.