Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5b9b6a5
feat(solana-indexer): add types module scaffolding
tilacog Jun 8, 2026
a938616
toml fmt
tilacog Jun 8, 2026
c7b2dbc
fix(ci): reformat TOML files with tombi 1.1.0
tilacog Jun 8, 2026
bfb4dcf
fix(solana-indexer): include slot fields in ReplayWindowExceeded erro…
tilacog Jun 23, 2026
6e1f41e
refactor(solana-indexer): rename as_str to as_label on Commitment and…
tilacog Jun 23, 2026
0fef0ed
refactor(solana-indexer): drop unused PartialEq+Eq from Commitment
tilacog Jun 23, 2026
f57158f
docs(solana-indexer): remove org/spec references from StreamUpdate co…
tilacog Jun 23, 2026
72146c4
Concise commitment-level doc comments in solana-indexer
tilacog Jun 23, 2026
399c2ec
docs: tighten commitment module doc-comment, name each type's role
tilacog Jun 23, 2026
cd72ab5
refactor(solana-indexer): use bytes::Bytes for opaque byte blobs
tilacog Jun 24, 2026
57b5c76
Add `Slot` newtype for Solana ledger slots
tilacog Jun 24, 2026
c7936f4
refactor(solana-indexer): introduce OrderUid newtype for order identi…
tilacog Jun 24, 2026
3d65b11
fix(toml): alphabetize bytes/derive_more in solana-indexer/Cargo.toml
tilacog Jun 24, 2026
f6f7cb8
Use u64 for auction_id in solana SettlementFinalized domain type
tilacog Jun 24, 2026
64166cb
docs(solana-indexer): clarify that TradeDelta.order_fulfilled is infe…
tilacog Jun 24, 2026
fe154a4
nit: fmt comments
tilacog Jun 24, 2026
271539d
docs(solana-indexer): clarify SolFlowEvent::OrderEnabled doc comment
tilacog Jun 24, 2026
6b2e8a1
chore(solana-indexer): Remove metrics module
tilacog Jun 24, 2026
362e86e
restrict types visibility to pub(crate) and suppress dead_code warnings
tilacog Jun 24, 2026
dfc1272
refactor(solana-indexer): extract Slot and OrderUid into dedicated mo…
tilacog Jun 24, 2026
47f8edf
fix: ix_index u8 → u16
tilacog Jun 25, 2026
135042e
Merge branch 'main' into solana-indexer/PR2-bootstrap
squadgazzz Jun 26, 2026
5092df3
Update cargo.lock
squadgazzz Jun 26, 2026
9f05387
Merge remote-tracking branch 'origin/main' into solana-indexer/PR2-bo…
squadgazzz Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,644 changes: 1,525 additions & 119 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ shared = { path = "crates/shared" }
signature-validator = { path = "crates/signature-validator" }
simulator = { path = "crates/simulator" }
solana-indexer = { path = "crates/solana-indexer" }
solana-sdk = "4"
solver = { path = "crates/solver" }
solvers = { path = "crates/solvers" }
solvers-dto = { path = "crates/solvers-dto" }
Expand Down Expand Up @@ -147,6 +148,7 @@ tracing-subscriber = { version = "0.3.22", features = ["json"] }
url = "2.5.0"
vergen = "8"
winner-selection = { path = "crates/winner-selection" }
yellowstone-grpc-proto = { version = "12.4.0", default-features = false }

[workspace.lints]
clippy.cast_possible_wrap = "deny"
8 changes: 8 additions & 0 deletions crates/solana-indexer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,13 @@ path = "src/lib.rs"
name = "solana-indexer"
path = "src/main.rs"

[dependencies]
bytes = { workspace = true }
derive_more = { workspace = true }
solana-sdk = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
yellowstone-grpc-proto = { workspace = true }

[lints]
workspace = true
2 changes: 2 additions & 0 deletions crates/solana-indexer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
//! `solana-indexer` — Solana settlement indexer.

pub mod types;
63 changes: 63 additions & 0 deletions crates/solana-indexer/src/types/channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#![expect(dead_code)]
//! Message types passed over the internal channels.
//!
//! The ingester pushes [`StreamUpdate`] into the channel to the decoder; the
//! decoder pushes [`PartialEvent`] / [`PartialHalf`] to the partial-event
//! watchdog.

use crate::types::{
Signature,
slot::Slot,
wire::{SubscribeUpdateAccountInfo, SubscribeUpdateTransactionInfo},
};

/// From `Ingester` → `Decoder`.
///
/// One multiplexed wire message, tagged with the slot the message was observed
/// at.
#[derive(Debug, Clone)]
pub(crate) enum StreamUpdate {
/// A transaction-update slot message.
Tx {
/// Slot the message was observed at.
slot: Slot,
/// Transaction signature.
signature: Signature,
/// Wire message body.
inner: Box<SubscribeUpdateTransactionInfo>,
},
/// An account-update slot message.
Account {
/// Slot the message was observed at.
slot: Slot,
/// Optional signature linking the write back to its originating
/// transaction.
txn_signature: Option<Signature>,
/// Wire message body.
inner: Box<SubscribeUpdateAccountInfo>,
},
}

/// From `Decoder` → `PartialEventWatchdog`.
///
/// The watchdog holds incomplete `(slot, signature)` pairs until both halves
/// arrive; each delivery carries the half that just landed.
#[derive(Debug, Clone, Copy)]
pub(crate) struct PartialEvent {
/// Slot the partial was observed at.
pub slot: Slot,
/// Transaction signature the partial corresponds to.
pub signature: Signature,
}
Comment on lines +45 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The doc comment for PartialEvent states that "each delivery carries the half that just landed," but the struct definition is missing the half: PartialHalf field. Without this field, the watchdog cannot receive or hold the actual data halves to match and reconstruct the full event. Additionally, since PartialHalf contains heap-allocated Box types, PartialEvent cannot derive Copy and should only derive Debug, Clone.

Suggested change
#[derive(Debug, Clone, Copy)]
pub struct PartialEvent {
/// Slot the partial was observed at.
pub slot: u64,
/// Transaction signature the partial corresponds to.
pub signature: Signature,
}
#[derive(Debug, Clone)]
pub struct PartialEvent {
/// Slot the partial was observed at.
pub slot: u64,
/// Transaction signature the partial corresponds to.
pub signature: Signature,
/// The half that just landed.
pub half: PartialHalf,
}


/// One of the two halves a [`StreamUpdate`] can produce.
///
/// The decoder pushes one `PartialEvent` per `StreamUpdate` it processes; the
/// watchdog uses the `(slot, signature)` key to match pairs.
#[derive(Debug, Clone)]
pub(crate) enum PartialHalf {
/// Transaction-update half.
Tx(Box<SubscribeUpdateTransactionInfo>),
/// Account-update half.
Account(Box<SubscribeUpdateAccountInfo>),
}
81 changes: 81 additions & 0 deletions crates/solana-indexer/src/types/commitment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#![expect(dead_code)]
//! Commitment-tracking types.
//!
//! This module holds the types we use to track how far a transaction has
//! progressed through Solana's commitment pipeline, plus the row shapes the
//! finalization worker reads and writes.
//!
//! The indexer captures transactions at `confirmed` commitment. A later
//! finalization pass polls `getSignatureStatuses` (whose result is modeled by
//! [`SignatureStatus`]) and either promotes the row to `finalized` or marks it
//! `rolled_back`. [`UnfinalizedRow`] is the shape the finalization worker
//! queries for when sweeping aged confirmed rows, and [`AccountInfo`] holds
//! account snapshots used for recovery when accounts aren't obtained normally
//! through the ingestion stream.

use {
crate::types::{Signature, slot::Slot},
bytes::Bytes,
solana_sdk::pubkey::Pubkey,
};

/// Commitment level persisted by the indexer.
///
/// Solana consensus defines `processed`, `confirmed`, and `finalized`
/// commitment levels, but we only store the two durable states plus a terminal
/// failure state for abandoned slots. `processed` is omitted because it
/// reflects the node's latest view and is still rollback-prone.
#[derive(Debug, Clone, Copy)]
pub(crate) enum Commitment {
/// Voted on by a supermajority but can still be rolled back. Watched by the
/// finalization worker.
Confirmed,
/// Rooted by the cluster and considered permanently settled.
Finalized,
/// Never landed, or its slot was abandoned by the cluster.
RolledBack,
}

impl Commitment {
/// String label used in `solana.*` `commitment` columns.
pub fn as_label(self) -> &'static str {
match self {
Self::Confirmed => "confirmed",
Self::Finalized => "finalized",
Self::RolledBack => "rolled_back",
}
}
Comment thread
jmg-duarte marked this conversation as resolved.
}

/// Result of an RPC `getSignatureStatuses` poll.
#[derive(Debug, Clone, Copy)]
pub(crate) struct SignatureStatus {
/// Slot the transaction landed at, if known.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That this data can be missing seems to indicate that this might better be an enum.

pub slot: Slot,
/// Confirmation status reported by the RPC.
pub confirmation_status: Commitment,
}

/// Snapshot of an account at a given slot (from `getAccountInfo`).
#[derive(Debug, Clone)]
pub(crate) struct AccountInfo {
/// Slot the snapshot was read at.
pub slot: Slot,
/// Account data (serialized).
pub data: Bytes,
/// Account owner program.
pub owner: Pubkey,
}

/// A `solana.*` row that has not yet reached `finalized` commitment — the kind
/// picked up by the aged-row sweep, where `commitment = 'confirmed'` and the
/// row's slot is at least one finalization window behind `LATEST_CHAIN_SLOT`.
#[derive(Debug, Clone)]
pub(crate) struct UnfinalizedRow {
/// Table the row lives in.
pub table: &'static str,
/// Transaction signature.
pub signature: Signature,
/// Slot the row was inserted at.
pub slot: Slot,
}
47 changes: 47 additions & 0 deletions crates/solana-indexer/src/types/dead_letter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#![expect(dead_code)]
//! Dead-letter types: events that failed to persist and were diverted to
//! `solana.dead_letter` for operator follow-up.

use {
crate::types::{Signature, slot::Slot},
bytes::Bytes,
};

/// A decoded event whose write to `solana.*` failed and was diverted to
/// `solana.dead_letter`.
#[derive(Debug, Clone)]
pub(crate) struct DeadLetterEntry {
/// Slot the event was observed at.
pub slot: Slot,
/// Transaction signature, if the failure was per-transaction.
pub signature: Option<Signature>,
/// Why the event landed in the dead-letter table.
pub reason: DeadLetterReason,
/// Original raw bytes for replay.
pub raw_bytes: Bytes,
}

/// Why a row landed in the dead-letter table.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DeadLetterReason {
/// Decoder received both halves but couldn't parse them.
DecoderError,
/// Watchdog gave up: account-update half never arrived.
AccountUpdateMissing,
/// Watchdog gave up: transaction-update half never arrived.
TxUpdateMissing,
/// Settlement landed but no `proposed_solutions` row matched.
SolutionUidUnmatchable,
}

impl DeadLetterReason {
/// String label used in `solana.dead_letter.reason`.
pub fn as_label(self) -> &'static str {
match self {
Self::DecoderError => "decoder_error",
Self::AccountUpdateMissing => "account_update_missing",
Self::TxUpdateMissing => "tx_update_missing",
Self::SolutionUidUnmatchable => "solution_uid_unmatchable",
}
}
}
58 changes: 58 additions & 0 deletions crates/solana-indexer/src/types/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#![expect(dead_code)]
//! Error types used across the indexer's domain.

use {crate::types::slot::Slot, thiserror::Error};

/// Failures surfaced from the decoder.
#[derive(Debug, Error, PartialEq, Eq)]
pub(crate) enum DecodeError {
/// The discriminator byte(s) at the start of the instruction data did not
/// match any known instruction on either program.
#[error("unknown instruction discriminator")]
UnknownDiscriminator,
/// The ALT (Address Lookup Table) loaded-address list could not be resolved
/// against the full account list.
#[error("alt resolution failed")]
AltResolutionFailed,
/// The instruction was recognised but its schema did not match the on-chain
/// layout.
#[error("schema mismatch")]
SchemaMismatch,
}

/// Failures surfaced from the persistence boundary.
#[derive(Debug, Error, PartialEq, Eq)]
pub(crate) enum StoreError {
/// The SQL `ON CONFLICT` clause rejected the write (e.g. watermark
/// regression).
#[error("store conflict")]
Conflict,
/// The store is temporarily unavailable (e.g. connection lost, pool
/// exhausted). The caller is expected to retry.
#[error("store unavailable")]
Unavailable,
}

/// Failures surfaced from the stream boundary.
#[derive(Debug, Error)]
pub(crate) enum StreamError {
/// The stream has been disconnected by the server.
#[error("stream disconnected")]
Disconnected,
/// The internal mpsc send timed out (backpressure on the decoder).
#[error("stream send timeout")]
SendTimeout,
/// The resume slot is outside the provider's replay window. The caller
/// should reset `from_slot` to `LATEST_CHAIN_SLOT − replay_window`,
/// record the lost range, and retry the subscription.
#[error(
"replay window exceeded: attempted slot {attempted_slot}, earliest replayable \
{earliest_replayable_slot}"
)]
ReplayWindowExceeded {
/// The slot the subscriber attempted to resume from.
attempted_slot: Slot,
/// The earliest slot the provider can still serve.
earliest_replayable_slot: Slot,
},
}
Loading
Loading