# Test utilities (Carbon 2.0)

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

`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](https://carbon.sevenlabs.org/guides/testing.md).

`Cargo.toml`:

```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<()>`.

| Item                                                                                                        | Purpose                                                   |
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| [`read_instruction`](#read_instruction)                                                                     | Load a `solana_instruction::Instruction` from JSON.       |
| [`read_account`](#read_account)                                                                             | Load a `solana_account::Account` from JSON.               |
| [`read_transaction_meta`](#read_transaction_meta)                                                           | Load a `UiTransactionStatusMeta` (the RPC `meta` object). |
| [`TestInstruction`](#testinstruction), [`TestAccountMeta`](#testaccountmeta), [`TestAccount`](#testaccount) | The JSON shapes the loaders accept.                       |
| [`base58_deserialize::ix_data`](#base58_deserializeix_data)                                                 | Decode 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`](#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`:

```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](#example-decoder-tests), [`InstructionDecoder`](https://carbon.sevenlabs.org/reference/traits.md).

## `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`](#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`:

```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`](https://carbon.sevenlabs.org/reference/traits.md).

## `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`:

```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`](https://carbon.sevenlabs.org/reference/processor-inputs.md).

## `TestInstruction`

The JSON shape [`read_instruction`](#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.**

| Field        | JSON type                                      | Description                    |
| ------------ | ---------------------------------------------- | ------------------------------ |
| `program_id` | base58 string                                  | Program the instruction calls. |
| `accounts`   | array of [`TestAccountMeta`](#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.**

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

**See also.** [`read_instruction`](#read_instruction).

## `TestAccountMeta`

One account of a [`TestInstruction`](#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.**

| 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.**

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

**See also.** [`TestInstruction`](#testinstruction).

## `TestAccount`

The JSON shape [`read_account`](#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.**

| 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.**

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

**See also.** [`read_account`](#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).

## Example: decoder tests

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

`Cargo.toml`:

```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`:

```rust
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]);
}
```

```sh
cargo test --test decode
```

Output:

```text
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
```

---

Carbon 2.0.0 documentation · Web: https://carbon.sevenlabs.org/reference/test-utils · Last updated 2026-11-09 · Index of all pages: https://carbon.sevenlabs.org/llms.txt
