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.
[dev-dependencies]
carbon-test-utils = "2.0.0"The crate exposes three loaders, three fixture types and one module. The loaders return anyhow::; add anyhow to your dev-dependencies only if your tests return anyhow::.
| Item | Purpose |
|---|---|
read_ | Load a solana_ from JSON. |
read_ | Load a solana_ from JSON. |
read_ | Load a UiTransactionStatusMeta (the RPC meta object). |
TestInstruction, TestAccountMeta, TestAccount | The JSON shapes the loaders accept. |
base58_ | Decode a base58 string into bytes. |
read_ instruction#
Reads a JSON file and returns the instruction it describes.
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/ works. Errors are Couldn't read fixture: … when the file cannot be read and Couldn't deserialize fixture: … when the JSON does not match.
{
"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.
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.
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. into data and the other fields across, renaming rentEpoch to rent_epoch.
{
"data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMqaOwAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"executable": false,
"lamports": 1461600,
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"rent_epoch": 18446744073709551615
}Example.
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.
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_.
{
"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.
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_ accepts.
#[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::InstructionDetails.
| Field | JSON type | Description |
|---|---|---|
program_id | base58 string | Program the instruction calls. |
accounts | array of TestAccountMeta | Accounts in instruction order. |
data | hex string, no 0x prefix | Instruction data. |
Deserialize-only: it does not implement Serialize, so you cannot write fixtures with it.
Example.
let fixture: carbon_test_utils::TestInstruction = serde_json::from_str(json)?;
let instruction: solana_instruction::Instruction = fixture.into();See also. read_.
TestAccountMeta#
One account of a TestInstruction.
#[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::AccountMetaDetails.
| Field | JSON type | Description |
|---|---|---|
pubkey | base58 string | Account address. |
is_signer | boolean | Whether the account signs. |
is_writable | boolean | Whether the account is writable. |
Example.
{ "pubkey": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", "is_signer": true, "is_writable": false }See also. TestInstruction.
TestAccount#
The JSON shape read_ accepts.
#[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::AccountDetails.
| Field | JSON type | Description |
|---|---|---|
data | base64 string (standard alphabet, padded) | Account data. |
executable | boolean | Whether the account is a program. |
lamports | number | Balance in lamports. |
owner | base58 string | Owning program. |
rent_epoch | number | Rent epoch; 18446744073709551615 (u64::MAX) for rent-exempt accounts. |
Example.
let fixture: carbon_test_utils::TestAccount = serde_json::from_str(json)?;
let account: solana_account::Account = fixture.into();See also. read_.
base58_ deserialize:: ix_ data#
Decodes a base58 string into bytes.
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.
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/.
[dependencies]
carbon-core = "2.0.0"
carbon-token-program-decoder = "2.0.0"
[dev-dependencies]
carbon-test-utils = "2.0.0"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]);
}cargo test --test decoderunning 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