1. API reference

Test utilities

The carbon-test-utils API — JSON fixture loaders for instructions, accounts and transaction metadata, and a base58 helper for decoder tests.

On this page

carbon-test-utils loads Solana instructions, accounts and transaction metadata from JSON fixture files so you can unit-test decoders and processors without a network. Use this page when you write decoder tests; for a walkthrough of testing a pipeline, see Testing.

Cargo.toml
[dev-dependencies]
carbon-test-utils = "2.0.0"

The crate exposes three loaders, three fixture types and one module. The loaders return anyhow::Result; add anyhow to your dev-dependencies only if your tests return anyhow::Result<()>.

ItemPurpose
read_instructionLoad a solana_instruction::Instruction from JSON.
read_accountLoad a solana_account::Account from JSON.
read_transaction_metaLoad a UiTransactionStatusMeta (the RPC meta object).
TestInstruction, TestAccountMeta, TestAccountThe JSON shapes the loaders accept.
base58_deserialize::ix_dataDecode a base58 string into bytes.

read_instruction#

Reads a JSON file and returns the instruction it describes.

Rust
pub fn read_instruction<P: AsRef<Path>>(ix_path: P) -> anyhow::Result<solana_instruction::Instruction>

Details. The file must match TestInstruction: program_id and each pubkey as base58 strings, data as a hex string. Paths are relative to the current directory; cargo test runs tests from the package root, so tests/fixtures/… works. Errors are Couldn't read fixture: … when the file cannot be read and Couldn't deserialize fixture: … when the JSON does not match.

tests/fixtures/transfer_ix.json
{
  "program_id": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
  "accounts": [
    { "pubkey": "7VHUFJHWu2CuExkJcJrzhQPJ2oygupTWkL2A2For4BmE", "is_signer": false, "is_writable": true },
    { "pubkey": "3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa", "is_signer": false, "is_writable": true },
    { "pubkey": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", "is_signer": true, "is_writable": false }
  ],
  "data": "03e803000000000000"
}

Example.

Rust
let instruction = carbon_test_utils::read_instruction("tests/fixtures/transfer_ix.json").unwrap();

See also. Example: decoder tests, InstructionDecoder.

read_account#

Reads a JSON file and returns the account it describes.

Rust
pub fn read_account<P: AsRef<Path>>(acc_path: P) -> anyhow::Result<solana_account::Account>

Details. The file must match TestAccount: data as base64 (the encoding getAccountInfo returns with "encoding": "base64"), owner as base58, lamports and rent_epoch as JSON numbers. This is not the raw RPC response: copy value.data[0] into data and the other fields across, renaming rentEpoch to rent_epoch.

tests/fixtures/mint_account.json
{
  "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMqaOwAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
  "executable": false,
  "lamports": 1461600,
  "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
  "rent_epoch": 18446744073709551615
}

Example.

Rust
let account = carbon_test_utils::read_account("tests/fixtures/mint_account.json").unwrap();

See also. AccountDecoder.

read_transaction_meta#

Reads a JSON file containing a transaction's status metadata in RPC form.

Rust
pub fn read_transaction_meta<P: AsRef<Path>>(
    tx_path: P,
) -> anyhow::Result<solana_transaction_status::UiTransactionStatusMeta>

Details. The file is the meta object of a getTransaction response (camelCase keys: fee, preBalances, innerInstructions, logMessages, …). Inner instructions must use the compiled form with base58 data, which is what "encoding": "json" returns. The conversion below replaces parsed (jsonParsed) inner instructions with empty placeholders and logs Unsupported instruction type encountered. To get the binary TransactionStatusMeta Carbon's pipeline uses, pass the result to carbon_core::transformers::transaction_metadata_from_original_meta.

tests/fixtures/tx_meta.json
{
  "err": null,
  "status": { "Ok": null },
  "fee": 5000,
  "preBalances": [1000000000, 0],
  "postBalances": [999995000, 0],
  "innerInstructions": [],
  "logMessages": ["Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"],
  "preTokenBalances": [],
  "postTokenBalances": [],
  "rewards": [],
  "loadedAddresses": { "writable": [], "readonly": [] },
  "computeUnitsConsumed": 150
}

Example.

Rust
let meta = carbon_test_utils::read_transaction_meta("tests/fixtures/tx_meta.json").unwrap();
let meta = carbon_core::transformers::transaction_metadata_from_original_meta(meta).unwrap();

See also. TransactionMetadata.

TestInstruction#

The JSON shape read_instruction accepts.

Rust
#[derive(Debug, Deserialize)]
pub struct TestInstruction {
    #[serde(with = "field_as_string")]
    pub program_id: Pubkey,
    pub accounts: Vec<TestAccountMeta>,
    #[serde(with = "hex_deserialize")]
    pub data: Vec<u8>,
}

impl From<TestInstruction> for solana_instruction::Instruction

Details.

FieldJSON typeDescription
program_idbase58 stringProgram the instruction calls.
accountsarray of TestAccountMetaAccounts in instruction order.
datahex string, no 0x prefixInstruction data.

Deserialize-only: it does not implement Serialize, so you cannot write fixtures with it.

Example.

Rust
let fixture: carbon_test_utils::TestInstruction = serde_json::from_str(json)?;
let instruction: solana_instruction::Instruction = fixture.into();

See also. read_instruction.

TestAccountMeta#

One account of a TestInstruction.

Rust
#[derive(Debug, Deserialize)]
pub struct TestAccountMeta {
    #[serde(with = "field_as_string")]
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
}

impl From<TestAccountMeta> for solana_instruction::AccountMeta

Details.

FieldJSON typeDescription
pubkeybase58 stringAccount address.
is_signerbooleanWhether the account signs.
is_writablebooleanWhether the account is writable.

Example.

JSON
{ "pubkey": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", "is_signer": true, "is_writable": false }

See also. TestInstruction.

TestAccount#

The JSON shape read_account accepts.

Rust
#[derive(Debug, Deserialize)]
pub struct TestAccount {
    #[serde(with = "base64_deserialize")]
    pub data: Vec<u8>,
    pub executable: bool,
    pub lamports: u64,
    #[serde(with = "field_as_string")]
    pub owner: Pubkey,
    pub rent_epoch: u64,
}

impl From<TestAccount> for solana_account::Account

Details.

FieldJSON typeDescription
database64 string (standard alphabet, padded)Account data.
executablebooleanWhether the account is a program.
lamportsnumberBalance in lamports.
ownerbase58 stringOwning program.
rent_epochnumberRent epoch; 18446744073709551615 (u64::MAX) for rent-exempt accounts.

Example.

Rust
let fixture: carbon_test_utils::TestAccount = serde_json::from_str(json)?;
let account: solana_account::Account = fixture.into();

See also. read_account.

base58_deserialize::ix_data#

Decodes a base58 string into bytes.

Rust
pub fn ix_data(ix: &str) -> Vec<u8>

Details. Use it to paste instruction data from an explorer or from a getTransaction response with "encoding": "json" (where data is base58) straight into a test. It panics on invalid base58; the panic message reads hex decode failed even though the input is base58. Despite the module name, it is a plain function, not a serde adapter.

Example.

Rust
use carbon_test_utils::base58_deserialize;

// [3, 232, 3, 0, 0, 0, 0, 0, 0]: an SPL Token Transfer of 1000.
let data = base58_deserialize::ix_data("3tGNFMqHiozw");

See also. Example: decoder tests.

Example: decoder tests#

Integration tests for the SPL Token decoder, using the three fixture files above saved under tests/fixtures/.

Cargo.toml
[dependencies]
carbon-core = "2.0.0"
carbon-token-program-decoder = "2.0.0"

[dev-dependencies]
carbon-test-utils = "2.0.0"
tests/decode.rs
use {
    carbon_core::{account::AccountDecoder, instruction::InstructionDecoder},
    carbon_test_utils::{base58_deserialize, read_account, read_instruction, read_transaction_meta},
    carbon_token_program_decoder::{
        accounts::TokenProgramAccount, instructions::TokenProgramInstruction, TokenProgramDecoder,
    },
};

#[test]
fn decodes_transfer_instruction() {
    let instruction = read_instruction("tests/fixtures/transfer_ix.json").unwrap();

    let decoded = TokenProgramDecoder
        .decode_instruction(&instruction)
        .expect("not a token program instruction");

    let TokenProgramInstruction::Transfer { data, accounts, .. } = decoded else {
        panic!("expected Transfer, got {decoded:?}");
    };
    assert_eq!(data.amount, 1_000);
    assert_eq!(
        accounts.authority.to_string(),
        "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1"
    );
}

#[test]
fn decodes_mint_account() {
    let account = read_account("tests/fixtures/mint_account.json").unwrap();

    let decoded = TokenProgramDecoder
        .decode_account(&account)
        .expect("not a token program account");

    let TokenProgramAccount::Mint(mint) = decoded.data else {
        panic!("expected Mint");
    };
    assert_eq!(mint.supply, 1_000_000_000);
    assert_eq!(mint.decimals, 6);
}

#[test]
fn loads_transaction_meta() {
    let meta = read_transaction_meta("tests/fixtures/tx_meta.json").unwrap();
    assert_eq!(meta.fee, 5000);

    // Convert the RPC (UI) shape into the binary meta Carbon's pipeline uses.
    let meta = carbon_core::transformers::transaction_metadata_from_original_meta(meta).unwrap();
    assert_eq!(meta.log_messages.unwrap().len(), 2);
}

#[test]
fn decodes_base58_instruction_data() {
    // Base58 of [3, 232, 3, 0, 0, 0, 0, 0, 0]: a token Transfer of 1000.
    let data = base58_deserialize::ix_data("3tGNFMqHiozw");
    assert_eq!(data, vec![3, 232, 3, 0, 0, 0, 0, 0, 0]);
}
Terminal
cargo test --test decode
Output
running 4 tests
test decodes_base58_instruction_data ... ok
test decodes_mint_account ... ok
test decodes_transfer_instruction ... ok
test loads_transaction_meta ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Your endpoints

Code samples use these instead of the placeholders, so you can copy and run them as they are. They’re stored only in this browser and never sent anywhere.