Browse Source

Update solana-program-test tests to mollusk (with helper harness) (#131)

#### Problem

Tests for the associated token account program still use solana-program-test and have limited reusability. Development going forward would benefit from up-to-date Mollusk tests and common utilities for testing and benching.

### Summary of changes

Updates the original solana-program-test tests for SPL ATA to use mollusk instead (https://github.com/anza-xyz/mollusk). Refactors reusable test logic into an `ata-mollusk-harness` with builder patterns, as a separate crate due to plans for its reuse by additional upcoming tests and benchmark tools.

Thanks to @buffalojoec @joncinque and @febo for review.
Peter Keay 1 month ago
parent
commit
4d4e86ee05

+ 19 - 0
.github/workflows/main.yml

@@ -45,6 +45,25 @@ jobs:
       - name: Lint
         run: pnpm interface:lint
 
+  format_and_lint_test_harness:
+    name: Format & Lint Test Harness
+    runs-on: ubuntu-latest
+    steps:
+      - name: Git Checkout
+        uses: actions/checkout@v4
+
+      - name: Setup Environment
+        uses: ./.github/actions/setup
+        with:
+          clippy: true
+          rustfmt: true
+
+      - name: Format
+        run: pnpm test-harness:format
+
+      - name: Lint
+        run: pnpm test-harness:lint
+
   audit_rust:
     name: Audit Rust
     runs-on: ubuntu-latest

File diff suppressed because it is too large
+ 28 - 691
Cargo.lock


+ 1 - 1
Cargo.toml

@@ -1,6 +1,6 @@
 [workspace]
 resolver = "2"
-members = ["interface", "program"]
+members = ["interface", "program", "test-harness"]
 
 [workspace.metadata.cli]
 solana = "3.0.0"

+ 4 - 1
package.json

@@ -12,7 +12,10 @@
     "rust:spellcheck": "cargo spellcheck --code 1",
     "rust:audit": "zx ./scripts/rust/audit.mjs",
     "rust:publish": "zx ./scripts/rust/publish.mjs",
-    "rust:semver": "cargo semver-checks"
+    "rust:semver": "cargo semver-checks",
+    "test-harness:format": "zx ./scripts/rust/format.mjs test-harness",
+    "test-harness:lint": "zx ./scripts/rust/lint.mjs test-harness",
+    "test-harness:test": "zx ./scripts/rust/test.mjs test-harness"
   },
   "devDependencies": {
     "@iarna/toml": "^2.2.5",

+ 8 - 2
program/Cargo.toml

@@ -18,6 +18,7 @@ num-traits = "0.2"
 solana-decode-error = "2.3.0"
 solana-program = "3.0"
 solana-pubkey = "3.0"
+solana-rent = "3.0"
 solana-system-interface = { version = "2.0", features = ["bincode"] }
 spl-associated-token-account-interface = { version = "2.0.0", features = ["borsh"] }
 spl-token-interface = "2.0.0"
@@ -25,8 +26,13 @@ spl-token-2022-interface = "2.0.0"
 thiserror = "2.0"
 
 [dev-dependencies]
-solana-program-test = "3.0"
-solana-sdk = "3.0"
+ata-mollusk-harness = { version = "1.0.0", path = "../test-harness" }
+mollusk-svm = { version = "0.6.0" }
+solana-instruction = "3.0"
+solana-keypair = "3.0"
+solana-program-error = "3.0"
+solana-signer = "3.0"
+solana-sysvar = "3.0"
 
 [lib]
 crate-type = ["cdylib", "lib"]

+ 82 - 229
program/tests/create_idempotent.rs

@@ -1,243 +1,96 @@
-mod program_test;
-
 use {
-    program_test::program_test_2022,
-    solana_program::instruction::*,
-    solana_program_test::*,
-    solana_pubkey::Pubkey,
-    solana_sdk::{
-        account::Account as SolanaAccount,
-        program_option::COption,
-        program_pack::Pack,
-        signature::Signer,
-        signer::keypair::Keypair,
-        transaction::{Transaction, TransactionError},
-    },
-    solana_system_interface::instruction::create_account,
-    spl_associated_token_account::error::AssociatedTokenAccountError,
-    spl_associated_token_account_interface::{
-        address::get_associated_token_address_with_program_id,
-        instruction::{
-            create_associated_token_account, create_associated_token_account_idempotent,
-        },
-    },
-    spl_token_2022_interface::{
-        extension::ExtensionType,
-        instruction::initialize_account,
-        state::{Account, AccountState},
+    ata_mollusk_harness::{
+        build_create_ata_instruction, token_2022_immutable_owner_account_len,
+        token_2022_immutable_owner_rent_exempt_balance, AtaTestHarness, CreateAtaInstructionType,
     },
+    mollusk_svm::result::Check,
+    solana_program_error::ProgramError,
+    solana_pubkey::Pubkey,
 };
 
-#[tokio::test]
-async fn success_account_exists() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let (mut banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-    let expected_token_account_len =
-        ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
-            .unwrap();
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    let instruction = create_associated_token_account_idempotent(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
-    );
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // Associated account now exists
-    let associated_account = banks_client
-        .get_account(associated_token_address)
-        .await
-        .expect("get_account")
-        .expect("associated_account not none");
-    assert_eq!(associated_account.data.len(), expected_token_account_len);
-    assert_eq!(associated_account.owner, spl_token_2022_interface::id());
-    assert_eq!(associated_account.lamports, expected_token_account_balance);
-
-    // Unchecked instruction fails
-    let instruction = create_associated_token_account(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
-    );
-    assert_eq!(
-        banks_client
-            .process_transaction(transaction)
-            .await
-            .unwrap_err()
-            .unwrap(),
-        TransactionError::InstructionError(0, InstructionError::IllegalOwner)
-    );
-
-    // Get a new blockhash, succeed with create if non existent
-    let recent_blockhash = banks_client
-        .get_new_latest_blockhash(&recent_blockhash)
-        .await
+#[test]
+fn success_account_exists() {
+    let mut harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
+    // CreateIdempotent will create the ATA if it doesn't exist
+    let ata_address = harness.create_ata(CreateAtaInstructionType::CreateIdempotent { bump: None });
+    let associated_account = harness
+        .ctx
+        .account_store
+        .borrow()
+        .get(&ata_address)
+        .cloned()
         .unwrap();
 
-    let instruction = create_associated_token_account_idempotent(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
+    // Failure case: try to Create when ATA already exists as token account
+    harness
+        .ctx
+        .account_store
+        .borrow_mut()
+        .insert(ata_address, associated_account.clone());
+    let instruction = build_create_ata_instruction(
+        spl_associated_token_account::id(),
+        harness.payer,
+        ata_address,
+        harness.wallet.unwrap(),
+        harness.mint.unwrap(),
+        spl_token_2022_interface::id(),
+        CreateAtaInstructionType::default(),
+    );
+    harness
+        .ctx
+        .process_and_validate_instruction(&instruction, &[Check::err(ProgramError::IllegalOwner)]);
+
+    // But CreateIdempotent should succeed when account exists
+    let instruction = build_create_ata_instruction(
+        spl_associated_token_account::id(),
+        harness.payer,
+        ata_address,
+        harness.wallet.unwrap(),
+        harness.mint.unwrap(),
+        spl_token_2022_interface::id(),
+        CreateAtaInstructionType::CreateIdempotent { bump: None },
+    );
+    harness.ctx.process_and_validate_instruction(
+        &instruction,
+        &[
+            Check::success(),
+            Check::account(&ata_address)
+                .space(token_2022_immutable_owner_account_len())
+                .owner(&spl_token_2022_interface::id())
+                .lamports(token_2022_immutable_owner_rent_exempt_balance())
+                .build(),
+        ],
     );
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // Associated account is unchanged
-    let associated_account = banks_client
-        .get_account(associated_token_address)
-        .await
-        .expect("get_account")
-        .expect("associated_account not none");
-    assert_eq!(associated_account.data.len(), expected_token_account_len);
-    assert_eq!(associated_account.owner, spl_token_2022_interface::id());
-    assert_eq!(associated_account.lamports, expected_token_account_balance);
 }
 
-#[tokio::test]
-async fn fail_account_exists_with_wrong_owner() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
+#[test]
+fn fail_account_exists_with_wrong_owner() {
+    let harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
     let wrong_owner = Pubkey::new_unique();
-    let mut associated_token_account =
-        SolanaAccount::new(1_000_000_000, Account::LEN, &spl_token_2022_interface::id());
-    let token_account = Account {
-        mint: token_mint_address,
-        owner: wrong_owner,
-        amount: 0,
-        delegate: COption::None,
-        state: AccountState::Initialized,
-        is_native: COption::None,
-        delegated_amount: 0,
-        close_authority: COption::None,
-    };
-    Account::pack(token_account, &mut associated_token_account.data).unwrap();
-    let mut pt = program_test_2022(token_mint_address);
-    pt.add_account(associated_token_address, associated_token_account);
-    let (banks_client, payer, recent_blockhash) = pt.start().await;
-
-    // fail creating token account if non existent
-    let instruction = create_associated_token_account_idempotent(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
-    );
-
-    assert_eq!(
-        banks_client
-            .process_transaction(transaction)
-            .await
-            .unwrap_err()
-            .unwrap(),
-        TransactionError::InstructionError(
-            0,
-            InstructionError::Custom(AssociatedTokenAccountError::InvalidOwner as u32)
-        )
+    let ata_address = harness.insert_wrong_owner_token_account(wrong_owner);
+    let instruction = build_create_ata_instruction(
+        spl_associated_token_account::id(),
+        harness.payer,
+        ata_address,
+        harness.wallet.unwrap(),
+        harness.mint.unwrap(),
+        spl_token_2022_interface::id(),
+        CreateAtaInstructionType::CreateIdempotent { bump: None },
+    );
+    harness.ctx.process_and_validate_instruction(
+        &instruction,
+        &[Check::err(ProgramError::Custom(
+            spl_associated_token_account::error::AssociatedTokenAccountError::InvalidOwner as u32,
+        ))],
     );
 }
 
-#[tokio::test]
-async fn fail_non_ata() {
-    let token_mint_address = Pubkey::new_unique();
-    let (banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-
-    let rent = banks_client.get_rent().await.unwrap();
-    let token_account_len =
-        ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
-            .unwrap();
-    let token_account_balance = rent.minimum_balance(token_account_len);
-
-    let wallet_address = Pubkey::new_unique();
-    let account = Keypair::new();
-    let transaction = Transaction::new_signed_with_payer(
-        &[
-            create_account(
-                &payer.pubkey(),
-                &account.pubkey(),
-                token_account_balance,
-                token_account_len as u64,
-                &spl_token_2022_interface::id(),
-            ),
-            initialize_account(
-                &spl_token_2022_interface::id(),
-                &account.pubkey(),
-                &token_mint_address,
-                &wallet_address,
-            )
-            .unwrap(),
-        ],
-        Some(&payer.pubkey()),
-        &[&payer, &account],
-        recent_blockhash,
-    );
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    let mut instruction = create_associated_token_account_idempotent(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-    instruction.accounts[1] = AccountMeta::new(account.pubkey(), false); // <-- Invalid associated_account_address
-
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
-    );
-    assert_eq!(
-        banks_client
-            .process_transaction(transaction)
-            .await
-            .unwrap_err()
-            .unwrap(),
-        TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
-    );
+#[test]
+fn fail_non_ata() {
+    let harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
+    let wrong_account = Pubkey::new_unique();
+    harness.execute_with_wrong_account_address(wrong_account, ProgramError::InvalidSeeds);
 }

+ 62 - 184
program/tests/extended_mint.rs

@@ -1,215 +1,93 @@
-mod program_test;
-
 use {
-    program_test::program_test_2022,
-    solana_program::{instruction::*, pubkey::Pubkey},
-    solana_program_test::*,
-    solana_sdk::{
-        signature::Signer,
-        signer::keypair::Keypair,
-        transaction::{Transaction, TransactionError},
-    },
-    solana_system_interface::instruction as system_instruction,
-    spl_associated_token_account_interface::{
-        address::get_associated_token_address_with_program_id,
-        instruction::create_associated_token_account,
-    },
+    ata_mollusk_harness::AtaTestHarness,
+    mollusk_svm::result::Check,
+    solana_program_error::ProgramError,
     spl_token_2022_interface::{
-        error::TokenError,
         extension::{
             transfer_fee, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned,
         },
-        state::{Account, Mint},
+        state::Account,
     },
 };
 
-#[tokio::test]
-async fn test_associated_token_account_with_transfer_fees() {
-    let wallet_sender = Keypair::new();
-    let wallet_address_sender = wallet_sender.pubkey();
-    let wallet_address_receiver = Pubkey::new_unique();
-    let (mut banks_client, payer, recent_blockhash) =
-        program_test_2022(Pubkey::new_unique()).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-
-    // create extended mint
-    // ... in the future, a mint can be pre-loaded in program_test.rs like the
-    // regular mint
-    let mint_account = Keypair::new();
-    let token_mint_address = mint_account.pubkey();
-    let mint_authority = Keypair::new();
-    let space =
-        ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TransferFeeConfig])
-            .unwrap();
+#[test]
+fn test_associated_token_account_with_transfer_fees() {
     let maximum_fee = 100;
-    let mut transaction = Transaction::new_with_payer(
-        &[
-            system_instruction::create_account(
-                &payer.pubkey(),
-                &mint_account.pubkey(),
-                rent.minimum_balance(space),
-                space as u64,
-                &spl_token_2022_interface::id(),
-            ),
-            transfer_fee::instruction::initialize_transfer_fee_config(
-                &spl_token_2022_interface::id(),
-                &token_mint_address,
-                Some(&mint_authority.pubkey()),
-                Some(&mint_authority.pubkey()),
-                1_000,
-                maximum_fee,
-            )
-            .unwrap(),
-            spl_token_2022_interface::instruction::initialize_mint(
-                &spl_token_2022_interface::id(),
-                &token_mint_address,
-                &mint_authority.pubkey(),
-                Some(&mint_authority.pubkey()),
-                0,
-            )
-            .unwrap(),
-        ],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer, &mint_account], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // create extended ATAs
-    let mut transaction = Transaction::new_with_payer(
-        &[create_associated_token_account(
-            &payer.pubkey(),
-            &wallet_address_sender,
-            &token_mint_address,
-            &spl_token_2022_interface::id(),
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    let recent_blockhash = banks_client
-        .get_new_latest_blockhash(&recent_blockhash)
-        .await
-        .unwrap();
-
-    let mut transaction = Transaction::new_with_payer(
-        &[create_associated_token_account(
-            &payer.pubkey(),
-            &wallet_address_receiver,
-            &token_mint_address,
-            &spl_token_2022_interface::id(),
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    let associated_token_address_sender = get_associated_token_address_with_program_id(
-        &wallet_address_sender,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-    let associated_token_address_receiver = get_associated_token_address_with_program_id(
-        &wallet_address_receiver,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
+    let transfer_fee_basis_points = 1_000;
+    let (harness, receiver_wallet) = AtaTestHarness::new(&spl_token_2022_interface::id())
+        .with_wallet(1_000_000)
+        .with_additional_wallet(1_000_000);
+    let mut harness = harness
+        .with_mint_with_extensions(&[ExtensionType::TransferFeeConfig])
+        .initialize_transfer_fee(transfer_fee_basis_points, maximum_fee)
+        .initialize_mint(0)
+        .with_ata();
+    let (sender_pubkey, mint, sender_ata, receiver_ata) = (
+        harness.wallet.unwrap(),
+        harness.mint.unwrap(),
+        harness.ata_address.unwrap(),
+        harness.create_ata_for_owner(receiver_wallet, 1_000_000),
     );
+    harness.mint_tokens(50 * maximum_fee);
 
-    // mint tokens
-    let sender_amount = 50 * maximum_fee;
-    let mut transaction = Transaction::new_with_payer(
-        &[spl_token_2022_interface::instruction::mint_to(
+    // Insufficient funds transfer
+    harness.ctx.process_and_validate_instruction(
+        &transfer_fee::instruction::transfer_checked_with_fee(
             &spl_token_2022_interface::id(),
-            &token_mint_address,
-            &associated_token_address_sender,
-            &mint_authority.pubkey(),
-            &[],
-            sender_amount,
-        )
-        .unwrap()],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer, &mint_authority], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // not enough tokens
-    let mut transaction = Transaction::new_with_payer(
-        &[transfer_fee::instruction::transfer_checked_with_fee(
-            &spl_token_2022_interface::id(),
-            &associated_token_address_sender,
-            &token_mint_address,
-            &associated_token_address_receiver,
-            &wallet_address_sender,
+            &sender_ata,
+            &mint,
+            &receiver_ata,
+            &sender_pubkey,
             &[],
             10_001,
             0,
             maximum_fee,
         )
-        .unwrap()],
-        Some(&payer.pubkey()),
+        .unwrap(),
+        &[Check::err(ProgramError::Custom(
+            spl_token_2022_interface::error::TokenError::InsufficientFunds as u32,
+        ))],
     );
-    transaction.sign(&[&payer, &wallet_sender], recent_blockhash);
-    let err = banks_client
-        .process_transaction(transaction)
-        .await
-        .unwrap_err()
-        .unwrap();
-    assert_eq!(
-        err,
-        TransactionError::InstructionError(
-            0,
-            InstructionError::Custom(TokenError::InsufficientFunds as u32)
-        )
-    );
-
-    let recent_blockhash = banks_client
-        .get_new_latest_blockhash(&recent_blockhash)
-        .await
-        .unwrap();
 
-    // success
-    let transfer_amount = 500;
-    let fee = 50;
-    let mut transaction = Transaction::new_with_payer(
-        &[transfer_fee::instruction::transfer_checked_with_fee(
+    // Successful transfer
+    let (transfer_amount, fee) = (500, 50);
+    harness.ctx.process_and_validate_instruction(
+        &transfer_fee::instruction::transfer_checked_with_fee(
             &spl_token_2022_interface::id(),
-            &associated_token_address_sender,
-            &token_mint_address,
-            &associated_token_address_receiver,
-            &wallet_address_sender,
+            &sender_ata,
+            &mint,
+            &receiver_ata,
+            &sender_pubkey,
             &[],
             transfer_amount,
             0,
             fee,
         )
-        .unwrap()],
-        Some(&payer.pubkey()),
+        .unwrap(),
+        &[Check::success()],
     );
-    transaction.sign(&[&payer, &wallet_sender], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
 
-    let sender_account = banks_client
-        .get_account(associated_token_address_sender)
-        .await
-        .unwrap()
-        .unwrap();
-    let sender_state = StateWithExtensionsOwned::<Account>::unpack(sender_account.data).unwrap();
-    assert_eq!(sender_state.base.amount, sender_amount - transfer_amount);
-    let extension = sender_state
-        .get_extension::<transfer_fee::TransferFeeAmount>()
-        .unwrap();
-    assert_eq!(extension.withheld_amount, 0.into());
+    // Verify final account states
+    let sender_state =
+        StateWithExtensionsOwned::<Account>::unpack(harness.get_account(sender_ata).data).unwrap();
+    assert_eq!(sender_state.base.amount, 50 * maximum_fee - transfer_amount);
+    assert_eq!(
+        sender_state
+            .get_extension::<transfer_fee::TransferFeeAmount>()
+            .unwrap()
+            .withheld_amount,
+        0.into()
+    );
 
-    let receiver_account = banks_client
-        .get_account(associated_token_address_receiver)
-        .await
-        .unwrap()
-        .unwrap();
     let receiver_state =
-        StateWithExtensionsOwned::<Account>::unpack(receiver_account.data).unwrap();
+        StateWithExtensionsOwned::<Account>::unpack(harness.get_account(receiver_ata).data)
+            .unwrap();
     assert_eq!(receiver_state.base.amount, transfer_amount - fee);
-    let extension = receiver_state
-        .get_extension::<transfer_fee::TransferFeeAmount>()
-        .unwrap();
-    assert_eq!(extension.withheld_amount, fee.into());
+    assert_eq!(
+        receiver_state
+            .get_extension::<transfer_fee::TransferFeeAmount>()
+            .unwrap()
+            .withheld_amount,
+        fee.into()
+    );
 }

BIN
program/tests/fixtures/pinocchio_token_program.so


BIN
program/tests/fixtures/spl_token_2022.so


+ 115 - 284
program/tests/process_create_associated_token_account.rs

@@ -1,317 +1,148 @@
-mod program_test;
-
 use {
-    program_test::program_test_2022,
-    solana_program::{instruction::*, pubkey::Pubkey, sysvar},
-    solana_program_test::*,
-    solana_sdk::{
-        signature::Signer,
-        transaction::{Transaction, TransactionError},
-    },
-    solana_system_interface::instruction as system_instruction,
-    spl_associated_token_account_interface::{
-        address::get_associated_token_address_with_program_id,
-        instruction::create_associated_token_account,
+    ata_mollusk_harness::{
+        build_create_ata_instruction, token_2022_immutable_owner_rent_exempt_balance,
+        AtaTestHarness, CreateAtaInstructionType,
     },
-    spl_token_2022_interface::{extension::ExtensionType, state::Account},
+    mollusk_svm::result::Check,
+    solana_instruction::AccountMeta,
+    solana_program_error::ProgramError,
+    solana_pubkey::Pubkey,
+    solana_sysvar as sysvar,
+    spl_associated_token_account_interface::address::get_associated_token_address_with_program_id,
 };
 
-#[tokio::test]
-async fn test_associated_token_address() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let (banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-
-    let expected_token_account_len =
-        ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
-            .unwrap();
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    // Associated account does not exist
-    assert_eq!(
-        banks_client
-            .get_account(associated_token_address)
-            .await
-            .expect("get_account"),
-        None,
-    );
-
-    let mut transaction = Transaction::new_with_payer(
-        &[create_associated_token_account(
-            &payer.pubkey(),
-            &wallet_address,
-            &token_mint_address,
-            &spl_token_2022_interface::id(),
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // Associated account now exists
-    let associated_account = banks_client
-        .get_account(associated_token_address)
-        .await
-        .expect("get_account")
-        .expect("associated_account not none");
-    assert_eq!(associated_account.data.len(), expected_token_account_len,);
-    assert_eq!(associated_account.owner, spl_token_2022_interface::id());
-    assert_eq!(associated_account.lamports, expected_token_account_balance);
+#[test]
+fn test_associated_token_address() {
+    let mut harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
+    harness.create_ata(CreateAtaInstructionType::default());
 }
 
-#[tokio::test]
-async fn test_create_with_fewer_lamports() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let (banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-    let expected_token_account_len =
-        ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
-            .unwrap();
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    // Transfer lamports into `associated_token_address` before creating it - enough
-    // to be rent-exempt for 0 data, but not for an initialized token account
-    let mut transaction = Transaction::new_with_payer(
-        &[system_instruction::transfer(
-            &payer.pubkey(),
-            &associated_token_address,
-            rent.minimum_balance(0),
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    assert_eq!(
-        banks_client
-            .get_balance(associated_token_address)
-            .await
-            .unwrap(),
-        rent.minimum_balance(0)
-    );
-
-    // Check that the program adds the extra lamports
-    let mut transaction = Transaction::new_with_payer(
-        &[create_associated_token_account(
-            &payer.pubkey(),
-            &wallet_address,
-            &token_mint_address,
-            &spl_token_2022_interface::id(),
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    assert_eq!(
-        banks_client
-            .get_balance(associated_token_address)
-            .await
-            .unwrap(),
-        expected_token_account_balance,
-    );
-}
+#[test]
+fn test_create_with_fewer_lamports() {
+    let harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
 
-#[tokio::test]
-async fn test_create_with_excess_lamports() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
+    let wallet = harness.wallet.unwrap();
+    let mint = harness.mint.unwrap();
+    let ata_address = get_associated_token_address_with_program_id(
+        &wallet,
+        &mint,
         &spl_token_2022_interface::id(),
     );
 
-    let (banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
+    let insufficient_lamports = 890880;
+    harness.ensure_account_exists_with_lamports(ata_address, insufficient_lamports);
 
-    let expected_token_account_len =
-        ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
-            .unwrap();
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    // Transfer 1 lamport into `associated_token_address` before creating it
-    let mut transaction = Transaction::new_with_payer(
-        &[system_instruction::transfer(
-            &payer.pubkey(),
-            &associated_token_address,
-            expected_token_account_balance + 1,
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    assert_eq!(
-        banks_client
-            .get_balance(associated_token_address)
-            .await
-            .unwrap(),
-        expected_token_account_balance + 1
+    let instruction = build_create_ata_instruction(
+        spl_associated_token_account_interface::program::id(),
+        harness.payer,
+        ata_address,
+        wallet,
+        mint,
+        spl_token_2022_interface::id(),
+        CreateAtaInstructionType::default(),
     );
 
-    // Check that the program doesn't add any lamports
-    let mut transaction = Transaction::new_with_payer(
-        &[create_associated_token_account(
-            &payer.pubkey(),
-            &wallet_address,
-            &token_mint_address,
-            &spl_token_2022_interface::id(),
-        )],
-        Some(&payer.pubkey()),
-    );
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    assert_eq!(
-        banks_client
-            .get_balance(associated_token_address)
-            .await
-            .unwrap(),
-        expected_token_account_balance + 1
+    harness.ctx.process_and_validate_instruction(
+        &instruction,
+        &[
+            Check::success(),
+            Check::account(&ata_address)
+                .lamports(token_2022_immutable_owner_rent_exempt_balance())
+                .owner(&spl_token_2022_interface::id())
+                .build(),
+        ],
     );
 }
 
-#[tokio::test]
-async fn test_create_account_mismatch() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let _associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-
-    let (banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-
-    let mut instruction = create_associated_token_account(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
-    instruction.accounts[1] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid associated_account_address
-
-    let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
-    transaction.sign(&[&payer], recent_blockhash);
-    assert_eq!(
-        banks_client
-            .process_transaction(transaction)
-            .await
-            .unwrap_err()
-            .unwrap(),
-        TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
-    );
+#[test]
+fn test_create_with_excess_lamports() {
+    let harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
 
-    let mut instruction = create_associated_token_account(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
+    let wallet = harness.wallet.unwrap();
+    let mint = harness.mint.unwrap();
+    let ata_address = get_associated_token_address_with_program_id(
+        &wallet,
+        &mint,
         &spl_token_2022_interface::id(),
     );
-    instruction.accounts[2] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid wallet_address
 
-    let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
-    transaction.sign(&[&payer], recent_blockhash);
-    assert_eq!(
-        banks_client
-            .process_transaction(transaction)
-            .await
-            .unwrap_err()
-            .unwrap(),
-        TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
-    );
+    let excess_lamports = token_2022_immutable_owner_rent_exempt_balance() + 1;
+    harness.ensure_account_exists_with_lamports(ata_address, excess_lamports);
 
-    let mut instruction = create_associated_token_account(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
+    let instruction = build_create_ata_instruction(
+        spl_associated_token_account_interface::program::id(),
+        harness.payer,
+        ata_address,
+        wallet,
+        mint,
+        spl_token_2022_interface::id(),
+        CreateAtaInstructionType::default(),
     );
-    instruction.accounts[3] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid token_mint_address
 
-    let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
-    transaction.sign(&[&payer], recent_blockhash);
-    assert_eq!(
-        banks_client
-            .process_transaction(transaction)
-            .await
-            .unwrap_err()
-            .unwrap(),
-        TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
+    harness.ctx.process_and_validate_instruction(
+        &instruction,
+        &[
+            Check::success(),
+            Check::account(&ata_address)
+                .lamports(excess_lamports)
+                .owner(&spl_token_2022_interface::id())
+                .build(),
+        ],
     );
 }
 
-#[tokio::test]
-async fn test_create_associated_token_account_using_legacy_implicit_instruction() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address = get_associated_token_address_with_program_id(
-        &wallet_address,
-        &token_mint_address,
-        &spl_token_2022_interface::id(),
-    );
+#[test]
+fn test_create_account_mismatch() {
+    let harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
 
-    let (banks_client, payer, recent_blockhash) =
-        program_test_2022(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-    let expected_token_account_len =
-        ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
-            .unwrap();
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    // Associated account does not exist
-    assert_eq!(
-        banks_client
-            .get_account(associated_token_address)
-            .await
-            .expect("get_account"),
-        None,
-    );
-
-    let mut create_associated_token_account_ix = create_associated_token_account(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
+    let wallet = harness.wallet.unwrap();
+    let mint = harness.mint.unwrap();
+    let ata_address = get_associated_token_address_with_program_id(
+        &wallet,
+        &mint,
         &spl_token_2022_interface::id(),
     );
 
-    // Use implicit  instruction and rent account to replicate the legacy invocation
-    create_associated_token_account_ix.data = vec![];
-    create_associated_token_account_ix
-        .accounts
-        .push(AccountMeta::new_readonly(sysvar::rent::id(), false));
+    for account_idx in [1, 2, 3] {
+        let mut instruction = build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            harness.payer,
+            ata_address,
+            wallet,
+            mint,
+            spl_token_2022_interface::id(),
+            CreateAtaInstructionType::default(),
+        );
+
+        instruction.accounts[account_idx] = if account_idx == 1 {
+            AccountMeta::new(Pubkey::default(), false)
+        } else {
+            AccountMeta::new_readonly(Pubkey::default(), false)
+        };
+
+        harness.ctx.process_and_validate_instruction(
+            &instruction,
+            &[Check::err(ProgramError::InvalidSeeds)],
+        );
+    }
+}
 
-    let mut transaction =
-        Transaction::new_with_payer(&[create_associated_token_account_ix], Some(&payer.pubkey()));
-    transaction.sign(&[&payer], recent_blockhash);
-    banks_client.process_transaction(transaction).await.unwrap();
+#[test]
+fn test_create_associated_token_account_using_legacy_implicit_instruction() {
+    let mut harness =
+        AtaTestHarness::new(&spl_token_2022_interface::id()).with_wallet_and_mint(1_000_000, 6);
 
-    // Associated account now exists
-    let associated_account = banks_client
-        .get_account(associated_token_address)
-        .await
-        .expect("get_account")
-        .expect("associated_account not none");
-    assert_eq!(associated_account.data.len(), expected_token_account_len);
-    assert_eq!(associated_account.owner, spl_token_2022_interface::id());
-    assert_eq!(associated_account.lamports, expected_token_account_balance);
+    harness.create_and_check_ata_with_custom_instruction(
+        CreateAtaInstructionType::default(),
+        |instruction| {
+            instruction.data = vec![];
+            instruction
+                .accounts
+                .push(AccountMeta::new_readonly(sysvar::rent::id(), false));
+        },
+    );
 }

+ 0 - 52
program/tests/program_test.rs

@@ -1,52 +0,0 @@
-use {
-    solana_program_test::ProgramTest, solana_pubkey::Pubkey,
-    spl_associated_token_account_interface::program::id,
-};
-
-#[allow(dead_code)]
-pub fn program_test(token_mint_address: Pubkey) -> ProgramTest {
-    let mut pc = ProgramTest::new("spl_associated_token_account", id(), None);
-
-    // Add a token mint account
-    //
-    // The account data was generated by running:
-    //      $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
-    //                       --output-file tests/fixtures/token-mint-data.bin
-    //
-    pc.add_account_with_file_data(
-        token_mint_address,
-        1461600,
-        spl_token_interface::id(),
-        "token-mint-data.bin",
-    );
-
-    // Dial down the BPF compute budget to detect if the program gets bloated in the
-    // future
-    pc.set_compute_max_units(60_000);
-
-    pc
-}
-
-#[allow(dead_code)]
-pub fn program_test_2022(token_mint_address: Pubkey) -> ProgramTest {
-    let mut pc = ProgramTest::new("spl_associated_token_account", id(), None);
-
-    // Add a token mint account
-    //
-    // The account data was generated by running:
-    //      $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
-    //                       --output-file tests/fixtures/token-mint-data.bin
-    //
-    pc.add_account_with_file_data(
-        token_mint_address,
-        1461600,
-        spl_token_2022_interface::id(),
-        "token-mint-data.bin",
-    );
-
-    // Dial down the BPF compute budget to detect if the program gets bloated in the
-    // future
-    pc.set_compute_max_units(50_000);
-
-    pc
-}

+ 278 - 607
program/tests/recover_nested.rs

@@ -1,676 +1,347 @@
-mod program_test;
-
 use {
-    program_test::{program_test, program_test_2022},
-    solana_program::pubkey::Pubkey,
-    solana_program_test::*,
-    solana_sdk::{
-        instruction::{AccountMeta, InstructionError},
-        signature::Signer,
-        signer::keypair::Keypair,
-        transaction::{Transaction, TransactionError},
-    },
-    solana_system_interface::instruction as system_instruction,
+    ata_mollusk_harness::AtaTestHarness,
+    mollusk_svm::result::Check,
+    solana_instruction::AccountMeta,
+    solana_program_error::ProgramError,
+    solana_pubkey::Pubkey,
     spl_associated_token_account_interface::{
         address::get_associated_token_address_with_program_id, instruction,
     },
-    spl_token_2022_interface::{
-        extension::{ExtensionType, StateWithExtensionsOwned},
-        state::{Account, Mint},
-    },
+    spl_token_2022_interface::extension::StateWithExtensionsOwned,
 };
 
-async fn create_mint(context: &mut ProgramTestContext, program_id: &Pubkey) -> (Pubkey, Keypair) {
-    let mint_account = Keypair::new();
-    let token_mint_address = mint_account.pubkey();
-    let mint_authority = Keypair::new();
-    let space = ExtensionType::try_calculate_account_len::<Mint>(&[]).unwrap();
-    let rent = context.banks_client.get_rent().await.unwrap();
-    let transaction = Transaction::new_signed_with_payer(
+const TEST_MINT_AMOUNT: u64 = 100;
+
+fn test_recover_nested_same_mint(program_id: &Pubkey) {
+    let mut harness = AtaTestHarness::new(program_id)
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
+
+    let mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+
+    // Create nested ATA and mint tokens to it (not to the main, canonical ATA)
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
+
+    // Capture pre-state for lamports transfer validation
+    let wallet_pubkey = harness.wallet.unwrap();
+    let pre_wallet_lamports = {
+        let store = harness.ctx.account_store.borrow();
+        store.get(&wallet_pubkey).unwrap().lamports
+    };
+    let nested_lamports = harness.get_account(nested_ata).lamports;
+
+    // Build and execute recover instruction
+    let recover_instruction = harness.build_recover_nested_instruction(mint, mint);
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
         &[
-            system_instruction::create_account(
-                &context.payer.pubkey(),
-                &mint_account.pubkey(),
-                rent.minimum_balance(space),
-                space as u64,
-                program_id,
-            ),
-            spl_token_2022_interface::instruction::initialize_mint(
-                program_id,
-                &token_mint_address,
-                &mint_authority.pubkey(),
-                Some(&mint_authority.pubkey()),
-                0,
-            )
-            .unwrap(),
+            Check::success(),
+            // Wallet received nested account lamports
+            Check::account(&wallet_pubkey)
+                .lamports(pre_wallet_lamports.checked_add(nested_lamports).unwrap())
+                .build(),
+            // Nested account has no lamports
+            Check::account(&nested_ata).lamports(0).build(),
+            // Nested account is closed
+            Check::account(&nested_ata).closed().build(),
         ],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &mint_account],
-        context.last_blockhash,
     );
-    context
-        .banks_client
-        .process_transaction(transaction)
-        .await
-        .unwrap();
-    (token_mint_address, mint_authority)
+
+    // Validate the recovery worked - tokens should be in the destination ATA (owner_ata)
+    let destination_account = harness.get_account(owner_ata);
+    let destination_amount =
+        StateWithExtensionsOwned::<spl_token_2022_interface::state::Account>::unpack(
+            destination_account.data,
+        )
+        .unwrap()
+        .base
+        .amount;
+    assert_eq!(destination_amount, TEST_MINT_AMOUNT);
 }
 
-async fn create_associated_token_account(
-    context: &mut ProgramTestContext,
-    owner: &Pubkey,
-    mint: &Pubkey,
-    program_id: &Pubkey,
-) -> Pubkey {
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::create_associated_token_account(
-            &context.payer.pubkey(),
-            owner,
-            mint,
-            program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer],
-        context.last_blockhash,
-    );
-    context
-        .banks_client
-        .process_transaction(transaction)
-        .await
-        .unwrap();
+fn test_fail_missing_wallet_signature(token_program_id: &Pubkey) {
+    let mut harness = AtaTestHarness::new(token_program_id)
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
 
-    get_associated_token_address_with_program_id(owner, mint, program_id)
-}
+    let mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
 
-#[allow(clippy::too_many_arguments)]
-async fn try_recover_nested(
-    context: &mut ProgramTestContext,
-    program_id: &Pubkey,
-    nested_mint: Pubkey,
-    nested_mint_authority: Keypair,
-    nested_associated_token_address: Pubkey,
-    destination_token_address: Pubkey,
-    wallet: Keypair,
-    recover_transaction: Transaction,
-    expected_error: Option<InstructionError>,
-) {
-    let nested_account = context
-        .banks_client
-        .get_account(nested_associated_token_address)
-        .await
-        .unwrap()
-        .unwrap();
-    let lamports = nested_account.lamports;
-
-    // mint to nested account
-    let amount = 100;
-    let transaction = Transaction::new_signed_with_payer(
-        &[spl_token_2022_interface::instruction::mint_to(
-            program_id,
-            &nested_mint,
-            &nested_associated_token_address,
-            &nested_mint_authority.pubkey(),
-            &[],
-            amount,
-        )
-        .unwrap()],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &nested_mint_authority],
-        context.last_blockhash,
+    let mut recover_instruction = harness.build_recover_nested_instruction(mint, mint);
+    recover_instruction.accounts[5] = AccountMeta::new(harness.wallet.unwrap(), false);
+
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::MissingRequiredSignature)],
     );
-    context
-        .banks_client
-        .process_transaction(transaction)
-        .await
-        .unwrap();
-
-    // transfer / close nested account
-    let result = context
-        .banks_client
-        .process_transaction(recover_transaction)
-        .await;
-
-    if let Some(expected_error) = expected_error {
-        let error = result.unwrap_err().unwrap();
-        assert_eq!(error, TransactionError::InstructionError(0, expected_error));
-    } else {
-        result.unwrap();
-        // nested account is gone
-        assert!(context
-            .banks_client
-            .get_account(nested_associated_token_address)
-            .await
-            .unwrap()
-            .is_none());
-        let destination_account = context
-            .banks_client
-            .get_account(destination_token_address)
-            .await
-            .unwrap()
-            .unwrap();
-        let destination_state =
-            StateWithExtensionsOwned::<Account>::unpack(destination_account.data).unwrap();
-        assert_eq!(destination_state.base.amount, amount);
-        let wallet_account = context
-            .banks_client
-            .get_account(wallet.pubkey())
-            .await
-            .unwrap()
-            .unwrap();
-        assert_eq!(wallet_account.lamports, lamports);
-    }
 }
 
-async fn check_same_mint(context: &mut ProgramTestContext, program_id: &Pubkey) {
-    let wallet = Keypair::new();
-    let (mint, mint_authority) = create_mint(context, program_id).await;
+fn test_fail_wrong_signer(token_program_id: &Pubkey) {
+    let mut harness = AtaTestHarness::new(token_program_id)
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
 
-    let owner_associated_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
-    let nested_associated_token_address = create_associated_token_account(
-        context,
-        &owner_associated_token_address,
-        &mint,
-        program_id,
-    )
-    .await;
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::recover_nested(
-            &wallet.pubkey(),
-            &mint,
-            &mint,
-            program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
+    let mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
+
+    let wrong_wallet = Pubkey::new_unique();
+    harness.create_ata_for_owner(wrong_wallet, 1_000_000);
+
+    let recover_instruction =
+        instruction::recover_nested(&wrong_wallet, &mint, &mint, token_program_id);
+
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::IllegalOwner)],
     );
-    try_recover_nested(
-        context,
-        program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wallet,
-        transaction,
-        None,
-    )
-    .await;
 }
 
-#[tokio::test]
-async fn success_same_mint_2022() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_same_mint(&mut context, &spl_token_2022_interface::id()).await;
-}
+fn test_fail_not_nested(token_program_id: &Pubkey) {
+    let mut harness = AtaTestHarness::new(token_program_id)
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
 
-#[tokio::test]
-async fn success_same_mint() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_same_mint(&mut context, &spl_token_interface::id()).await;
-}
+    let mint = harness.mint.unwrap();
+    let wrong_wallet = Pubkey::new_unique();
+
+    let nested_ata = harness.create_ata_for_owner(wrong_wallet, 0);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
 
-async fn check_different_mints(context: &mut ProgramTestContext, program_id: &Pubkey) {
-    let wallet = Keypair::new();
-    let (owner_mint, _owner_mint_authority) = create_mint(context, program_id).await;
-    let (nested_mint, nested_mint_authority) = create_mint(context, program_id).await;
-
-    let owner_associated_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &owner_mint, program_id).await;
-    let nested_associated_token_address = create_associated_token_account(
-        context,
-        &owner_associated_token_address,
-        &nested_mint,
-        program_id,
-    )
-    .await;
-    let destination_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &nested_mint, program_id).await;
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::recover_nested(
-            &wallet.pubkey(),
-            &owner_mint,
-            &nested_mint,
-            program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
+    let recover_instruction = harness.build_recover_nested_instruction(mint, mint);
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::IllegalOwner)],
     );
-    try_recover_nested(
-        context,
-        program_id,
-        nested_mint,
-        nested_mint_authority,
-        nested_associated_token_address,
-        destination_token_address,
-        wallet,
-        transaction,
-        None,
-    )
-    .await;
 }
 
-#[tokio::test]
-async fn success_different_mints() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_different_mints(&mut context, &spl_token_interface::id()).await;
-}
+fn test_fail_wrong_address_derivation_owner(token_program_id: &Pubkey) {
+    let mut harness = AtaTestHarness::new(token_program_id)
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
 
-#[tokio::test]
-async fn success_different_mints_2022() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_different_mints(&mut context, &spl_token_2022_interface::id()).await;
-}
+    let mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
 
-async fn check_missing_wallet_signature(context: &mut ProgramTestContext, program_id: &Pubkey) {
-    let wallet = Keypair::new();
-    let (mint, mint_authority) = create_mint(context, program_id).await;
+    let mut recover_instruction = harness.build_recover_nested_instruction(mint, mint);
+    let wrong_owner_address = Pubkey::new_unique();
+    recover_instruction.accounts[3] = AccountMeta::new_readonly(wrong_owner_address, false);
 
-    let owner_associated_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
+    harness.ensure_accounts_with_lamports(&[(wrong_owner_address, 1_000_000)]);
 
-    let nested_associated_token_address = create_associated_token_account(
-        context,
-        &owner_associated_token_address,
-        &mint,
-        program_id,
-    )
-    .await;
-
-    let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, program_id);
-    recover.accounts[5] = AccountMeta::new(wallet.pubkey(), false);
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[recover],
-        Some(&context.payer.pubkey()),
-        &[&context.payer],
-        context.last_blockhash,
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::InvalidSeeds)],
     );
-    try_recover_nested(
-        context,
-        program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wallet,
-        transaction,
-        Some(InstructionError::MissingRequiredSignature),
-    )
-    .await;
 }
 
-#[tokio::test]
-async fn fail_missing_wallet_signature_2022() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_missing_wallet_signature(&mut context, &spl_token_2022_interface::id()).await;
+#[test]
+fn success_same_mint_2022() {
+    test_recover_nested_same_mint(&spl_token_2022_interface::id());
 }
 
-#[tokio::test]
-async fn fail_missing_wallet_signature() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_missing_wallet_signature(&mut context, &spl_token_interface::id()).await;
+#[test]
+fn success_same_mint() {
+    test_recover_nested_same_mint(&spl_token_interface::id());
 }
 
-async fn check_wrong_signer(context: &mut ProgramTestContext, program_id: &Pubkey) {
-    let wallet = Keypair::new();
-    let wrong_wallet = Keypair::new();
-    let (mint, mint_authority) = create_mint(context, program_id).await;
-
-    let owner_associated_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
-    let nested_associated_token_address = create_associated_token_account(
-        context,
-        &owner_associated_token_address,
-        &mint,
-        program_id,
-    )
-    .await;
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::recover_nested(
-            &wrong_wallet.pubkey(),
-            &mint,
-            &mint,
-            program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wrong_wallet],
-        context.last_blockhash,
+fn test_recover_nested_different_mints(program_id: &Pubkey) {
+    let harness = AtaTestHarness::new(program_id)
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
+
+    let owner_mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+
+    // Create a second mint for the nested token
+    let mut harness = harness.with_mint(0);
+    let nested_mint = harness.mint.unwrap();
+
+    // Create nested ATA and mint tokens to it
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
+
+    // Create destination ATA for the nested token
+    let destination_ata = harness.create_ata_for_owner(harness.wallet.unwrap(), 1_000_000);
+
+    // Capture pre-state for lamports transfer validation
+    let wallet_pubkey = harness.wallet.unwrap();
+    let pre_wallet_lamports = {
+        let store = harness.ctx.account_store.borrow();
+        store.get(&wallet_pubkey).unwrap().lamports
+    };
+    let nested_lamports = harness.get_account(nested_ata).lamports;
+
+    // Build and execute recover instruction
+    let recover_instruction = harness.build_recover_nested_instruction(owner_mint, nested_mint);
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[
+            Check::success(),
+            // Wallet received nested account lamports
+            Check::account(&wallet_pubkey)
+                .lamports(pre_wallet_lamports.checked_add(nested_lamports).unwrap())
+                .build(),
+            // Nested account has no lamports
+            Check::account(&nested_ata).lamports(0).build(),
+            // Nested account is closed
+            Check::account(&nested_ata).closed().build(),
+        ],
     );
-    try_recover_nested(
-        context,
-        program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wrong_wallet,
-        transaction,
-        Some(InstructionError::IllegalOwner),
-    )
-    .await;
+
+    // Validate the recovery worked - tokens should be in the destination ATA
+    let destination_account = harness.get_account(destination_ata);
+    let destination_amount =
+        StateWithExtensionsOwned::<spl_token_2022_interface::state::Account>::unpack(
+            destination_account.data,
+        )
+        .unwrap()
+        .base
+        .amount;
+    assert_eq!(destination_amount, TEST_MINT_AMOUNT);
 }
 
-#[tokio::test]
-async fn fail_wrong_signer_2022() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_wrong_signer(&mut context, &spl_token_2022_interface::id()).await;
+#[test]
+fn success_different_mints() {
+    test_recover_nested_different_mints(&spl_token_interface::id());
 }
 
-#[tokio::test]
-async fn fail_wrong_signer() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_wrong_signer(&mut context, &spl_token_interface::id()).await;
+#[test]
+fn success_different_mints_2022() {
+    test_recover_nested_different_mints(&spl_token_2022_interface::id());
 }
 
-async fn check_not_nested(context: &mut ProgramTestContext, program_id: &Pubkey) {
-    let wallet = Keypair::new();
-    let wrong_wallet = Pubkey::new_unique();
-    let (mint, mint_authority) = create_mint(context, program_id).await;
-
-    let owner_associated_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
-    let nested_associated_token_address =
-        create_associated_token_account(context, &wrong_wallet, &mint, program_id).await;
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::recover_nested(
-            &wallet.pubkey(),
-            &mint,
-            &mint,
-            program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
-    );
-    try_recover_nested(
-        context,
-        program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wallet,
-        transaction,
-        Some(InstructionError::IllegalOwner),
-    )
-    .await;
+#[test]
+fn fail_missing_wallet_signature_2022() {
+    test_fail_missing_wallet_signature(&spl_token_2022_interface::id());
 }
 
-#[tokio::test]
-async fn fail_not_nested_2022() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_not_nested(&mut context, &spl_token_2022_interface::id()).await;
+#[test]
+fn fail_missing_wallet_signature() {
+    test_fail_missing_wallet_signature(&spl_token_interface::id());
 }
 
-#[tokio::test]
-async fn fail_not_nested() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_not_nested(&mut context, &spl_token_interface::id()).await;
+#[test]
+fn fail_wrong_signer_2022() {
+    test_fail_wrong_signer(&spl_token_2022_interface::id());
 }
 
-async fn check_wrong_address_derivation_owner(
-    context: &mut ProgramTestContext,
-    program_id: &Pubkey,
-) {
-    let wallet = Keypair::new();
-    let wrong_wallet = Pubkey::new_unique();
-    let (mint, mint_authority) = create_mint(context, program_id).await;
+#[test]
+fn fail_wrong_signer() {
+    test_fail_wrong_signer(&spl_token_interface::id());
+}
 
-    let owner_associated_token_address =
-        create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
-    let nested_associated_token_address = create_associated_token_account(
-        context,
-        &owner_associated_token_address,
-        &mint,
-        program_id,
-    )
-    .await;
-
-    let wrong_owner_associated_token_address =
-        get_associated_token_address_with_program_id(&mint, &wrong_wallet, program_id);
-    let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, program_id);
-    recover.accounts[3] = AccountMeta::new(wrong_owner_associated_token_address, false);
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[recover],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
-    );
-    try_recover_nested(
-        context,
-        program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        wrong_owner_associated_token_address,
-        wallet,
-        transaction,
-        Some(InstructionError::InvalidSeeds),
-    )
-    .await;
+#[test]
+fn fail_not_nested_2022() {
+    test_fail_not_nested(&spl_token_2022_interface::id());
 }
 
-#[tokio::test]
-async fn fail_wrong_address_derivation_owner_2022() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_wrong_address_derivation_owner(&mut context, &spl_token_2022_interface::id()).await;
+#[test]
+fn fail_not_nested() {
+    test_fail_not_nested(&spl_token_interface::id());
+}
+#[test]
+fn fail_wrong_address_derivation_owner_2022() {
+    test_fail_wrong_address_derivation_owner(&spl_token_2022_interface::id());
 }
 
-#[tokio::test]
-async fn fail_wrong_address_derivation_owner() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_wrong_address_derivation_owner(&mut context, &spl_token_interface::id()).await;
+#[test]
+fn fail_wrong_address_derivation_owner() {
+    test_fail_wrong_address_derivation_owner(&spl_token_interface::id());
 }
 
-async fn check_owner_account_does_not_exist(context: &mut ProgramTestContext, program_id: &Pubkey) {
-    let wallet = Keypair::new();
-    let (mint, mint_authority) = create_mint(context, program_id).await;
+#[test]
+fn fail_owner_account_does_not_exist() {
+    let mut harness = AtaTestHarness::new(&spl_token_2022_interface::id())
+        .with_wallet(1_000_000)
+        .with_mint(0);
+    // Note: deliberately NOT calling .with_ata() - owner ATA should not exist
+
+    let mint = harness.mint.unwrap();
+    let wallet_pubkey = harness.wallet.unwrap();
+    let owner_ata_address = get_associated_token_address_with_program_id(
+        &wallet_pubkey,
+        &mint,
+        &spl_token_2022_interface::id(),
+    );
 
-    let owner_associated_token_address =
-        get_associated_token_address_with_program_id(&wallet.pubkey(), &mint, program_id);
-    let nested_associated_token_address = create_associated_token_account(
-        context,
-        &owner_associated_token_address,
+    // Create nested ATA using non-existent owner ATA address
+    let nested_ata = harness.create_ata_for_owner(owner_ata_address, 0);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
+
+    let recover_instruction = instruction::recover_nested(
+        &wallet_pubkey,
+        &mint,
         &mint,
-        program_id,
-    )
-    .await;
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::recover_nested(
-            &wallet.pubkey(),
-            &mint,
-            &mint,
-            program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
+        &spl_token_2022_interface::id(),
     );
-    try_recover_nested(
-        context,
-        program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wallet,
-        transaction,
-        Some(InstructionError::IllegalOwner),
-    )
-    .await;
-}
 
-#[tokio::test]
-async fn fail_owner_account_does_not_exist() {
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    check_owner_account_does_not_exist(&mut context, &spl_token_2022_interface::id()).await;
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::IllegalOwner)],
+    );
 }
 
-#[tokio::test]
-async fn fail_wrong_spl_token_program() {
-    let wallet = Keypair::new();
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let mut context = pt.start_with_context().await;
-    let program_id = spl_token_2022_interface::id();
-    let wrong_program_id = spl_token_interface::id();
-    let (mint, mint_authority) = create_mint(&mut context, &program_id).await;
-
-    let owner_associated_token_address =
-        create_associated_token_account(&mut context, &wallet.pubkey(), &mint, &program_id).await;
-    let nested_associated_token_address = create_associated_token_account(
-        &mut context,
-        &owner_associated_token_address,
+#[test]
+fn fail_wrong_spl_token_program() {
+    let mut harness = AtaTestHarness::new(&spl_token_2022_interface::id())
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
+
+    let mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
+
+    // Use wrong program in instruction
+    let recover_instruction = instruction::recover_nested(
+        &harness.wallet.unwrap(),
+        &mint,
         &mint,
-        &program_id,
-    )
-    .await;
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[instruction::recover_nested(
-            &wallet.pubkey(),
-            &mint,
-            &mint,
-            &wrong_program_id,
-        )],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
+        &spl_token_interface::id(), // Wrong program ID
+    );
+
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::IllegalOwner)],
     );
-    try_recover_nested(
-        &mut context,
-        &program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wallet,
-        transaction,
-        Some(InstructionError::IllegalOwner),
-    )
-    .await;
 }
 
-#[tokio::test]
-async fn fail_destination_not_wallet_ata() {
-    let wallet = Keypair::new();
+#[test]
+fn fail_destination_not_wallet_ata() {
+    let mut harness = AtaTestHarness::new(&spl_token_2022_interface::id())
+        .with_wallet(1_000_000)
+        .with_mint(0)
+        .with_ata();
+
+    let mint = harness.mint.unwrap();
+    let owner_ata = harness.ata_address.unwrap();
+    let nested_ata = harness.create_ata_for_owner(owner_ata, 1_000_000);
+    harness.mint_tokens_to(nested_ata, TEST_MINT_AMOUNT);
+
+    // Create wrong destination ATA
     let wrong_wallet = Pubkey::new_unique();
-    let dummy_mint = Pubkey::new_unique();
-    let pt = program_test_2022(dummy_mint);
-    let program_id = spl_token_2022_interface::id();
-    let mut context = pt.start_with_context().await;
-    let (mint, mint_authority) = create_mint(&mut context, &program_id).await;
-
-    let owner_associated_token_address =
-        create_associated_token_account(&mut context, &wallet.pubkey(), &mint, &program_id).await;
-    let nested_associated_token_address = create_associated_token_account(
-        &mut context,
-        &owner_associated_token_address,
-        &mint,
-        &program_id,
-    )
-    .await;
-    let wrong_destination_associated_token_account_address =
-        create_associated_token_account(&mut context, &wrong_wallet, &mint, &program_id).await;
-
-    let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, &program_id);
-    recover.accounts[2] =
-        AccountMeta::new(wrong_destination_associated_token_account_address, false);
-
-    context.last_blockhash = context
-        .banks_client
-        .get_new_latest_blockhash(&context.last_blockhash)
-        .await
-        .unwrap();
-    let transaction = Transaction::new_signed_with_payer(
-        &[recover],
-        Some(&context.payer.pubkey()),
-        &[&context.payer, &wallet],
-        context.last_blockhash,
+    let wrong_destination_ata = harness.create_ata_for_owner(wrong_wallet, 1_000_000);
+
+    let mut recover_instruction = harness.build_recover_nested_instruction(mint, mint);
+    recover_instruction.accounts[2] = AccountMeta::new(wrong_destination_ata, false);
+
+    harness.ctx.process_and_validate_instruction(
+        &recover_instruction,
+        &[Check::err(ProgramError::InvalidSeeds)],
     );
-    try_recover_nested(
-        &mut context,
-        &program_id,
-        mint,
-        mint_authority,
-        nested_associated_token_address,
-        owner_associated_token_address,
-        wallet,
-        transaction,
-        Some(InstructionError::InvalidSeeds),
-    )
-    .await;
 }

+ 22 - 102
program/tests/spl_token_create.rs

@@ -1,107 +1,27 @@
-mod program_test;
+use ata_mollusk_harness::{AtaTestHarness, CreateAtaInstructionType};
 
-#[allow(deprecated)]
-use spl_associated_token_account::create_associated_token_account as deprecated_create_associated_token_account;
-use {
-    program_test::program_test,
-    solana_program_test::*,
-    solana_pubkey::Pubkey,
-    solana_sdk::{program_pack::Pack, signature::Signer, transaction::Transaction},
-    spl_associated_token_account_interface::{
-        address::get_associated_token_address, instruction::create_associated_token_account,
-    },
-    spl_token_interface::state::Account,
-};
-
-#[tokio::test]
-async fn success_create() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address =
-        get_associated_token_address(&wallet_address, &token_mint_address);
-
-    let (banks_client, payer, recent_blockhash) = program_test(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-    let expected_token_account_len = Account::LEN;
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    // Associated account does not exist
-    assert_eq!(
-        banks_client
-            .get_account(associated_token_address)
-            .await
-            .expect("get_account"),
-        None,
-    );
-
-    let transaction = Transaction::new_signed_with_payer(
-        &[create_associated_token_account(
-            &payer.pubkey(),
-            &wallet_address,
-            &token_mint_address,
-            &spl_token_interface::id(),
-        )],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
-    );
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // Associated account now exists
-    let associated_account = banks_client
-        .get_account(associated_token_address)
-        .await
-        .expect("get_account")
-        .expect("associated_account not none");
-    assert_eq!(associated_account.data.len(), expected_token_account_len);
-    assert_eq!(associated_account.owner, spl_token_interface::id());
-    assert_eq!(associated_account.lamports, expected_token_account_balance);
+#[test]
+fn success_create() {
+    let mut harness =
+        AtaTestHarness::new(&spl_token_interface::id()).with_wallet_and_mint(1_000_000, 6);
+    harness.create_ata(CreateAtaInstructionType::default());
 }
 
-#[tokio::test]
-async fn success_using_deprecated_instruction_creator() {
-    let wallet_address = Pubkey::new_unique();
-    let token_mint_address = Pubkey::new_unique();
-    let associated_token_address =
-        get_associated_token_address(&wallet_address, &token_mint_address);
-
-    let (banks_client, payer, recent_blockhash) = program_test(token_mint_address).start().await;
-    let rent = banks_client.get_rent().await.unwrap();
-    let expected_token_account_len = Account::LEN;
-    let expected_token_account_balance = rent.minimum_balance(expected_token_account_len);
-
-    // Associated account does not exist
-    assert_eq!(
-        banks_client
-            .get_account(associated_token_address)
-            .await
-            .expect("get_account"),
-        None,
-    );
-
-    // Use legacy instruction creator
-    #[allow(deprecated)]
-    let create_associated_token_account_ix = deprecated_create_associated_token_account(
-        &payer.pubkey(),
-        &wallet_address,
-        &token_mint_address,
+#[test]
+fn success_using_deprecated_instruction_creator() {
+    let mut harness =
+        AtaTestHarness::new(&spl_token_interface::id()).with_wallet_and_mint(1_000_000, 6);
+
+    harness.create_and_check_ata_with_custom_instruction(
+        CreateAtaInstructionType::default(),
+        |instruction| {
+            instruction.data = vec![];
+            instruction
+                .accounts
+                .push(solana_instruction::AccountMeta::new_readonly(
+                    solana_sysvar::rent::id(),
+                    false,
+                ));
+        },
     );
-
-    let transaction = Transaction::new_signed_with_payer(
-        &[create_associated_token_account_ix],
-        Some(&payer.pubkey()),
-        &[&payer],
-        recent_blockhash,
-    );
-    banks_client.process_transaction(transaction).await.unwrap();
-
-    // Associated account now exists
-    let associated_account = banks_client
-        .get_account(associated_token_address)
-        .await
-        .expect("get_account")
-        .expect("associated_account not none");
-    assert_eq!(associated_account.data.len(), expected_token_account_len);
-    assert_eq!(associated_account.owner, spl_token_interface::id());
-    assert_eq!(associated_account.lamports, expected_token_account_balance);
 }

+ 1 - 0
scripts/solana.dic

@@ -49,3 +49,4 @@ staker/S
 APY
 codama
 autogenerated
+ATA

+ 27 - 0
test-harness/Cargo.toml

@@ -0,0 +1,27 @@
+[package]
+name = "ata-mollusk-harness"
+version = "1.0.0"
+edition = "2021"
+license = "Apache-2.0"
+publish = false
+
+[lib]
+crate-type = ["lib"]
+
+[dependencies]
+mollusk-svm = { version = "0.6.0" }
+mollusk-svm-programs-token = { version = "0.6.0" }
+solana-account = "3.0"
+solana-instruction = "3.0"
+solana-keypair = "3.0"
+solana-program-error = "3.0"
+solana-program-option = "3.0"
+solana-program-pack = "3.0"
+solana-pubkey = "3.0"
+solana-rent = "3.0"
+solana-signer = "3.0"
+solana-system-interface = { version = "2.0", features = ["bincode"] }
+solana-sysvar = "3.0"
+spl-associated-token-account-interface = "2.0"
+spl-token-interface = "2.0"
+spl-token-2022-interface = "2.0"

+ 590 - 0
test-harness/src/lib.rs

@@ -0,0 +1,590 @@
+use {
+    mollusk_svm::{program::loader_keys::LOADER_V3, result::Check, Mollusk, MolluskContext},
+    solana_account::Account,
+    solana_instruction::{AccountMeta, Instruction},
+    solana_program_error::ProgramError,
+    solana_program_option::COption,
+    solana_program_pack::Pack,
+    solana_pubkey::Pubkey,
+    solana_rent::Rent,
+    solana_system_interface::program as system_program,
+    solana_sysvar::rent,
+    spl_associated_token_account_interface::address::get_associated_token_address_with_program_id,
+    spl_token_2022_interface::{extension::ExtensionType, state::Account as Token2022Account},
+    spl_token_interface::{state::Account as TokenAccount, state::AccountState, state::Mint},
+    std::{collections::HashMap, vec::Vec},
+};
+
+/// Setup mollusk with local ATA and token programs
+pub fn setup_mollusk_with_programs(token_program_id: &Pubkey) -> Mollusk {
+    let ata_program_id = spl_associated_token_account_interface::program::id();
+    let mut mollusk = Mollusk::new(&ata_program_id, "spl_associated_token_account");
+
+    if *token_program_id == spl_token_2022_interface::id() {
+        mollusk.add_program(token_program_id, "spl_token_2022", &LOADER_V3);
+    } else {
+        mollusk.add_program(token_program_id, "pinocchio_token_program", &LOADER_V3);
+    }
+
+    mollusk
+}
+
+/// The type of ATA creation instruction to build.
+#[derive(Debug)]
+pub enum CreateAtaInstructionType {
+    /// The standard `Create` instruction, which can optionally include a bump seed and account length.
+    Create {
+        bump: Option<u8>,
+        account_len: Option<u16>,
+    },
+    /// The `CreateIdempotent` instruction, which can optionally include a bump seed.
+    CreateIdempotent { bump: Option<u8> },
+}
+
+impl Default for CreateAtaInstructionType {
+    fn default() -> Self {
+        Self::Create {
+            bump: None,
+            account_len: None,
+        }
+    }
+}
+
+/// Calculate the expected account length for a Token-2022 account with `ImmutableOwner` extension
+pub fn token_2022_immutable_owner_account_len() -> usize {
+    ExtensionType::try_calculate_account_len::<Token2022Account>(&[ExtensionType::ImmutableOwner])
+        .expect("Failed to calculate Token-2022 account length")
+}
+
+/// Calculate the rent-exempt balance for a Token-2022 account with `ImmutableOwner` extension
+pub fn token_2022_immutable_owner_rent_exempt_balance() -> u64 {
+    Rent::default().minimum_balance(token_2022_immutable_owner_account_len())
+}
+
+/// Calculate the rent-exempt balance for a standard SPL token account
+pub fn token_account_rent_exempt_balance() -> u64 {
+    Rent::default().minimum_balance(TokenAccount::LEN)
+}
+
+/// Test harness for ATA testing scenarios
+pub struct AtaTestHarness {
+    pub ctx: MolluskContext<HashMap<Pubkey, Account>>,
+    pub token_program_id: Pubkey,
+    pub payer: Pubkey,
+    pub wallet: Option<Pubkey>,
+    pub mint: Option<Pubkey>,
+    pub mint_authority: Option<Pubkey>,
+    pub ata_address: Option<Pubkey>,
+}
+
+impl AtaTestHarness {
+    /// Ensure an account exists in the context store with the given lamports.
+    /// If the account does not exist, it will be created as a system account.
+    /// However, this can be called on a non-system account (to be used for
+    /// example when testing accidental nested owners).
+    pub fn ensure_account_exists_with_lamports(&self, address: Pubkey, lamports: u64) {
+        let mut store = self.ctx.account_store.borrow_mut();
+        if let Some(existing) = store.get_mut(&address) {
+            if existing.lamports < lamports {
+                existing.lamports = lamports;
+            }
+        } else {
+            store.insert(address, AccountBuilder::system_account(lamports));
+        }
+    }
+
+    /// Ensure multiple accounts exist in the context store with the provided lamports
+    pub fn ensure_accounts_with_lamports(&self, entries: &[(Pubkey, u64)]) {
+        for (address, lamports) in entries.iter().copied() {
+            self.ensure_account_exists_with_lamports(address, lamports);
+        }
+    }
+
+    /// Internal: create the mint account owned by the token program with given space
+    fn create_mint_account(&mut self, mint_account: Pubkey, space: usize, mint_program_id: Pubkey) {
+        let mint_rent = Rent::default().minimum_balance(space);
+        let create_mint_ix = solana_system_interface::instruction::create_account(
+            &self.payer,
+            &mint_account,
+            mint_rent,
+            space as u64,
+            &mint_program_id,
+        );
+
+        self.ctx
+            .process_and_validate_instruction(&create_mint_ix, &[Check::success()]);
+    }
+
+    /// Create a new test harness with the specified token program
+    pub fn new(token_program_id: &Pubkey) -> Self {
+        let mollusk = setup_mollusk_with_programs(token_program_id);
+        let payer = Pubkey::new_unique();
+        let ctx = mollusk.with_context(HashMap::new());
+
+        let harness = Self {
+            ctx,
+            token_program_id: *token_program_id,
+            payer,
+            wallet: None,
+            mint: None,
+            mint_authority: None,
+            ata_address: None,
+        };
+        harness.ensure_account_exists_with_lamports(payer, 10_000_000_000);
+        harness
+    }
+
+    /// Add a wallet with the specified lamports
+    pub fn with_wallet(mut self, lamports: u64) -> Self {
+        let wallet = Pubkey::new_unique();
+        self.ensure_accounts_with_lamports(&[(wallet, lamports)]);
+        self.wallet = Some(wallet);
+        self
+    }
+
+    /// Add an additional wallet (e.g. for sender/receiver scenarios) - returns harness and the new wallet
+    pub fn with_additional_wallet(self, lamports: u64) -> (Self, Pubkey) {
+        let additional_wallet = Pubkey::new_unique();
+        self.ensure_accounts_with_lamports(&[(additional_wallet, lamports)]);
+        (self, additional_wallet)
+    }
+
+    /// Create and initialize a mint with the specified decimals
+    pub fn with_mint(mut self, decimals: u8) -> Self {
+        let [mint_authority, mint_account] = [Pubkey::new_unique(); 2];
+
+        self.create_mint_account(mint_account, Mint::LEN, self.token_program_id);
+
+        self.mint = Some(mint_account);
+        self.mint_authority = Some(mint_authority);
+        self.initialize_mint(decimals)
+    }
+
+    /// Create and initialize a Token-2022 mint with specific extensions
+    pub fn with_mint_with_extensions(mut self, extensions: &[ExtensionType]) -> Self {
+        if self.token_program_id != spl_token_2022_interface::id() {
+            panic!("with_mint_with_extensions() can only be used with Token-2022 program");
+        }
+
+        let [mint_authority, mint_account] = [Pubkey::new_unique(); 2];
+
+        // Calculate space needed for extensions
+        let space =
+            ExtensionType::try_calculate_account_len::<spl_token_2022_interface::state::Mint>(
+                extensions,
+            )
+            .expect("Failed to calculate mint space with extensions");
+
+        self.create_mint_account(mint_account, space, spl_token_2022_interface::id());
+
+        self.mint = Some(mint_account);
+        self.mint_authority = Some(mint_authority);
+        self
+    }
+
+    /// Initialize transfer fee extension on the current mint (requires Token-2022 mint with `TransferFeeConfig` extension)
+    pub fn initialize_transfer_fee(self, transfer_fee_basis_points: u16, maximum_fee: u64) -> Self {
+        let mint = self.mint.expect("Mint must be set");
+        let mint_authority = self.mint_authority.expect("Mint authority must be set");
+
+        let init_fee_ix = spl_token_2022_interface::extension::transfer_fee::instruction::initialize_transfer_fee_config(
+            &spl_token_2022_interface::id(),
+            &mint,
+            Some(&mint_authority),
+            Some(&mint_authority),
+            transfer_fee_basis_points,
+            maximum_fee,
+        )
+        .expect("Failed to create initialize_transfer_fee_config instruction");
+
+        self.ctx
+            .process_and_validate_instruction(&init_fee_ix, &[Check::success()]);
+        self
+    }
+
+    /// Initialize mint (must be called after extensions are initialized)
+    pub fn initialize_mint(self, decimals: u8) -> Self {
+        let mint = self.mint.expect("Mint must be set");
+        let mint_authority = self.mint_authority.expect("Mint authority must be set");
+
+        let init_mint_ix = spl_token_2022_interface::instruction::initialize_mint(
+            &self.token_program_id,
+            &mint,
+            &mint_authority,
+            Some(&mint_authority),
+            decimals,
+        )
+        .expect("Failed to create initialize_mint instruction");
+
+        self.ctx
+            .process_and_validate_instruction(&init_mint_ix, &[Check::success()]);
+        self
+    }
+
+    /// Create an ATA for the wallet and mint (requires wallet and mint to be set)
+    pub fn with_ata(mut self) -> Self {
+        let wallet = self.wallet.expect("Wallet must be set before creating ATA");
+        let mint = self.mint.expect("Mint must be set before creating ATA");
+
+        let ata_address =
+            get_associated_token_address_with_program_id(&wallet, &mint, &self.token_program_id);
+
+        let instruction = build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            self.payer,
+            ata_address,
+            wallet,
+            mint,
+            self.token_program_id,
+            CreateAtaInstructionType::default(),
+        );
+
+        self.ctx
+            .process_and_validate_instruction(&instruction, &[Check::success()]);
+
+        self.ata_address = Some(ata_address);
+        self
+    }
+
+    /// Get a reference to an account by pubkey
+    pub fn get_account(&self, pubkey: Pubkey) -> Account {
+        self.ctx
+            .account_store
+            .borrow()
+            .get(&pubkey)
+            .expect("account not found")
+            .clone()
+    }
+
+    /// Mint tokens to the ATA (requires `mint`, `mint_authority` and `ata_address` to be set)
+    pub fn mint_tokens(&mut self, amount: u64) {
+        let ata_address = self.ata_address.expect("ATA must be set");
+        self.mint_tokens_to(ata_address, amount);
+    }
+
+    /// Mint tokens to a specific address
+    pub fn mint_tokens_to(&mut self, destination: Pubkey, amount: u64) {
+        let mint = self.mint.expect("Mint must be set");
+        let mint_authority = self
+            .mint_authority
+            .as_ref()
+            .expect("Mint authority must be set");
+
+        let mint_to_ix = spl_token_2022_interface::instruction::mint_to(
+            &self.token_program_id,
+            &mint,
+            &destination,
+            mint_authority,
+            &[],
+            amount,
+        )
+        .unwrap();
+
+        self.ctx
+            .process_and_validate_instruction(&mint_to_ix, &[Check::success()]);
+    }
+
+    /// Build a create ATA instruction for the current wallet and mint
+    pub fn build_create_ata_instruction(
+        &mut self,
+        instruction_type: CreateAtaInstructionType,
+    ) -> solana_instruction::Instruction {
+        let wallet = self.wallet.expect("Wallet must be set");
+        let mint = self.mint.expect("Mint must be set");
+        let ata_address =
+            get_associated_token_address_with_program_id(&wallet, &mint, &self.token_program_id);
+
+        self.ata_address = Some(ata_address);
+
+        build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            self.payer,
+            ata_address,
+            wallet,
+            mint,
+            self.token_program_id,
+            instruction_type,
+        )
+    }
+
+    /// Create an ATA for any owner. Ensure the owner exists as a system account,
+    /// creating it with the given lamports if it does not exist.
+    pub fn create_ata_for_owner(&mut self, owner: Pubkey, owner_lamports: u64) -> Pubkey {
+        let mint = self.mint.expect("Mint must be set");
+        self.ensure_accounts_with_lamports(&[(owner, owner_lamports)]);
+
+        let ata_address =
+            get_associated_token_address_with_program_id(&owner, &mint, &self.token_program_id);
+
+        let instruction = build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            self.payer,
+            ata_address,
+            owner,
+            mint,
+            self.token_program_id,
+            CreateAtaInstructionType::default(),
+        );
+
+        self.ctx
+            .process_and_validate_instruction(&instruction, &[Check::success()]);
+
+        ata_address
+    }
+
+    /// Build a `recover_nested` instruction and ensure all required accounts exist
+    pub fn build_recover_nested_instruction(
+        &mut self,
+        owner_mint: Pubkey,
+        nested_mint: Pubkey,
+    ) -> solana_instruction::Instruction {
+        let wallet = self.wallet.as_ref().expect("Wallet must be set");
+
+        spl_associated_token_account_interface::instruction::recover_nested(
+            wallet,
+            &owner_mint,
+            &nested_mint,
+            &self.token_program_id,
+        )
+    }
+
+    /// Add a wallet and mint (convenience method)
+    pub fn with_wallet_and_mint(self, wallet_lamports: u64, decimals: u8) -> Self {
+        self.with_wallet(wallet_lamports).with_mint(decimals)
+    }
+
+    /// Build and execute a create ATA instruction
+    pub fn create_ata(&mut self, instruction_type: CreateAtaInstructionType) -> Pubkey {
+        let wallet = self.wallet.expect("Wallet must be set");
+        let mint = self.mint.expect("Mint must be set");
+        let ata_address =
+            get_associated_token_address_with_program_id(&wallet, &mint, &self.token_program_id);
+
+        let instruction = build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            self.payer,
+            ata_address,
+            wallet,
+            mint,
+            self.token_program_id,
+            instruction_type,
+        );
+
+        let expected_len = if self.token_program_id == spl_token_2022_interface::id() {
+            token_2022_immutable_owner_account_len()
+        } else {
+            TokenAccount::LEN
+        };
+
+        let expected_balance = if self.token_program_id == spl_token_2022_interface::id() {
+            token_2022_immutable_owner_rent_exempt_balance()
+        } else {
+            token_account_rent_exempt_balance()
+        };
+
+        self.ctx.process_and_validate_instruction(
+            &instruction,
+            &[
+                Check::success(),
+                Check::account(&ata_address)
+                    .space(expected_len)
+                    .owner(&self.token_program_id)
+                    .lamports(expected_balance)
+                    .build(),
+            ],
+        );
+
+        self.ata_address = Some(ata_address);
+        ata_address
+    }
+
+    /// Create a token account with wrong owner at the ATA address (for error testing)
+    pub fn insert_wrong_owner_token_account(&self, wrong_owner: Pubkey) -> Pubkey {
+        let wallet = self.wallet.as_ref().expect("Wallet must be set");
+        let mint = self.mint.expect("Mint must be set");
+        self.ensure_accounts_with_lamports(&[(wrong_owner, 1_000_000)]);
+        let ata_address =
+            get_associated_token_address_with_program_id(wallet, &mint, &self.token_program_id);
+        // Create token account with wrong owner at the ATA address
+        let wrong_account =
+            AccountBuilder::token_account(&mint, &wrong_owner, 0, &self.token_program_id);
+        self.ctx
+            .account_store
+            .borrow_mut()
+            .insert(ata_address, wrong_account);
+        ata_address
+    }
+
+    /// Execute an instruction with a modified account address (for testing non-ATA addresses)
+    pub fn execute_with_wrong_account_address(
+        &self,
+        wrong_account: Pubkey,
+        expected_error: ProgramError,
+    ) {
+        let wallet = self.wallet.expect("Wallet must be set");
+        let mint = self.mint.expect("Mint must be set");
+
+        // Create a token account at the wrong address
+        self.ctx.account_store.borrow_mut().insert(
+            wrong_account,
+            AccountBuilder::token_account(&mint, &wallet, 0, &self.token_program_id),
+        );
+
+        let mut instruction = build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            self.payer,
+            get_associated_token_address_with_program_id(&wallet, &mint, &self.token_program_id),
+            wallet,
+            mint,
+            self.token_program_id,
+            CreateAtaInstructionType::CreateIdempotent { bump: None },
+        );
+
+        // Replace the ATA address with the wrong account address
+        instruction.accounts[1] = AccountMeta::new(wrong_account, false);
+
+        self.ctx
+            .process_and_validate_instruction(&instruction, &[Check::err(expected_error)]);
+    }
+
+    /// Create ATA instruction with custom modifications (for special cases like legacy empty data)
+    pub fn create_and_check_ata_with_custom_instruction<F>(
+        &mut self,
+        instruction_type: CreateAtaInstructionType,
+        modify_instruction: F,
+    ) -> Pubkey
+    where
+        F: FnOnce(&mut solana_instruction::Instruction),
+    {
+        let wallet = self.wallet.expect("Wallet must be set");
+        let mint = self.mint.expect("Mint must be set");
+        let ata_address =
+            get_associated_token_address_with_program_id(&wallet, &mint, &self.token_program_id);
+
+        let mut instruction = build_create_ata_instruction(
+            spl_associated_token_account_interface::program::id(),
+            self.payer,
+            ata_address,
+            wallet,
+            mint,
+            self.token_program_id,
+            instruction_type,
+        );
+
+        // Apply custom modification
+        modify_instruction(&mut instruction);
+
+        let expected_len = if self.token_program_id == spl_token_2022_interface::id() {
+            token_2022_immutable_owner_account_len()
+        } else {
+            TokenAccount::LEN
+        };
+
+        let expected_balance = if self.token_program_id == spl_token_2022_interface::id() {
+            token_2022_immutable_owner_rent_exempt_balance()
+        } else {
+            token_account_rent_exempt_balance()
+        };
+
+        self.ctx.process_and_validate_instruction(
+            &instruction,
+            &[
+                Check::success(),
+                Check::account(&ata_address)
+                    .space(expected_len)
+                    .owner(&self.token_program_id)
+                    .lamports(expected_balance)
+                    .build(),
+            ],
+        );
+
+        self.ata_address = Some(ata_address);
+        ata_address
+    }
+}
+
+/// Encodes the instruction data payload for ATA creation-related instructions.
+pub fn encode_create_ata_instruction_data(instruction_type: &CreateAtaInstructionType) -> Vec<u8> {
+    match instruction_type {
+        CreateAtaInstructionType::Create { bump, account_len } => {
+            let mut data = vec![0]; // Discriminator for Create
+            if let Some(b) = bump {
+                data.push(*b);
+                if let Some(len) = account_len {
+                    data.extend_from_slice(&len.to_le_bytes());
+                }
+            }
+            data
+        }
+        CreateAtaInstructionType::CreateIdempotent { bump } => {
+            let mut data = vec![1]; // Discriminator for CreateIdempotent
+            if let Some(b) = bump {
+                data.push(*b);
+            }
+            data
+        }
+    }
+}
+
+/// Build a create associated token account instruction with a given discriminator
+pub fn build_create_ata_instruction(
+    ata_program_id: Pubkey,
+    payer: Pubkey,
+    ata_address: Pubkey,
+    wallet: Pubkey,
+    mint: Pubkey,
+    token_program: Pubkey,
+    instruction_type: CreateAtaInstructionType,
+) -> Instruction {
+    Instruction {
+        program_id: ata_program_id,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new(ata_address, false),
+            AccountMeta::new_readonly(wallet, false),
+            AccountMeta::new_readonly(mint, false),
+            AccountMeta::new_readonly(system_program::id(), false),
+            AccountMeta::new_readonly(token_program, false),
+            AccountMeta::new_readonly(rent::id(), false),
+        ],
+        data: encode_create_ata_instruction_data(&instruction_type),
+    }
+}
+
+pub struct AccountBuilder;
+
+impl AccountBuilder {
+    pub fn system_account(lamports: u64) -> Account {
+        Account {
+            lamports,
+            data: Vec::new(),
+            owner: solana_system_interface::program::id(),
+            executable: false,
+            rent_epoch: 0,
+        }
+    }
+
+    pub fn token_account(
+        mint: &Pubkey,
+        owner: &Pubkey,
+        amount: u64,
+        token_program: &Pubkey,
+    ) -> Account {
+        let account_data = TokenAccount {
+            mint: *mint,
+            owner: *owner,
+            amount,
+            delegate: COption::None,
+            state: AccountState::Initialized,
+            is_native: COption::None,
+            delegated_amount: 0,
+            close_authority: COption::None,
+        };
+
+        if *token_program == spl_token_2022_interface::id() {
+            mollusk_svm_programs_token::token2022::create_account_for_token_account(account_data)
+        } else {
+            mollusk_svm_programs_token::token::create_account_for_token_account(account_data)
+        }
+    }
+}

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