diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index 3d70b312fe..dbb5a0b9e3 100644 --- a/crates/solana-indexer/src/indexer/decoder.rs +++ b/crates/solana-indexer/src/indexer/decoder.rs @@ -1,10 +1,9 @@ -#![expect(dead_code)] +#![allow(dead_code, reason = "dead in the lib build, exercised by tests")] //! The decoder pulls `StreamUpdate`s from the ingester, decodes //! settlement-program and SolFlow transactions, and persists typed events. -// TODO: `decode_solflow` and the persist path are stubbed. `run` drains the -// channel, decodes settlement instructions into `SettlementEvent`s, and drops -// them until the persistence adapter lands. +// TODO: `decode_solflow` is a stub (no on-chain SolFlow program yet), and the +// `Persistence` write bodies are no-ops until the Postgres adapter lands. use { crate::{ @@ -13,7 +12,7 @@ use { Signature, channel::StreamUpdate, errors::{DecodeError, PersistenceError}, - events::{SettlementEvent, TradeDelta}, + events::{DecodedEvent, SettlementEvent, TradeDelta}, order::OrderUid, slot::Slot, tx::{ResolvedInstruction, TxContext}, @@ -68,8 +67,9 @@ impl Decoder { } } - /// Main loop. Drains the channel and decodes each transaction's tracked - /// instructions. Returns when the ingester drops the sender. + /// Main loop. Drains the channel, decodes each transaction's tracked + /// instructions, and hands the results to the persistence seam. Returns + /// when the ingester drops the sender. pub async fn run(&mut self) -> Result<(), PersistenceError> { while let Some(update) = self.rx.recv().await { let StreamUpdate::Tx { @@ -77,35 +77,77 @@ impl Decoder { signature, inner, } = update; - self.decode(&inner, slot, signature); + let (events, decode_failed) = self.decode(&inner, slot, signature); + tracing::debug!(slot = %slot, event_count = events.len(), "decoded events"); + + // Stream resume is slot-granular (`from_slot = watermark + 1`), and + // the slot may still have more transactions in flight, so marking it + // done here could skip its remaining transactions after a crash. + // Writing `slot - 1` on every transaction only ever marks fully + // delivered slots. A redelivery of this slot after a restart is + // absorbed by idempotent writes. + let watermark = Slot(u64::from(slot).saturating_sub(1)); + if events.is_empty() { + self.persistence.write_watermark(watermark).await?; + } else { + // The watermark advance rides the same call as the events so the + // adapter can apply both in one SQL transaction. + self.persistence.persist_events(events, watermark).await?; + } + if decode_failed { + // Partial decode is expected: events that did decode persist and + // the watermark advances. Recovery replays the whole transaction + // by signature, and idempotent writes absorb the overlap. + self.persistence.write_dead_letter(signature, slot).await?; + } } Ok(()) } - /// Decode one transaction's tracked instructions into domain events. The - /// settlement half runs through the pure [`decode_settlement`]; the SolFlow - /// half is still stubbed. - fn decode(&self, tx: &SubscribeUpdateTransactionInfo, slot: Slot, signature: Signature) { + /// Decode one transaction's tracked instructions into domain events, plus + /// a flag reporting whether any settlement instruction failed to decode. + /// The settlement half runs through the pure [`decode_settlement`], the + /// SolFlow half is a stub. + #[tracing::instrument(skip_all, fields(slot = %slot, signature = %signature))] + fn decode( + &self, + tx: &SubscribeUpdateTransactionInfo, + slot: Slot, + signature: Signature, + ) -> (Vec, bool) { + // `meta` carries whether the transaction succeeded, so without it there is + // no way to tell, and emitting events for a transaction that may have + // reverted is the failure this guard exists to prevent. Dead-letter it + // instead of skipping: replay re-fetches by signature, and + // `getTransaction` returns the meta. + let Some(meta) = tx.meta.as_ref() else { + tracing::warn!("transaction update without meta"); + return (Vec::new(), true); + }; + + // A failed Solana transaction rolls back every account write, so no + // state-changing event may be emitted for it. Failed transactions are + // delivered on purpose (the revert is an attribution signal), and a + // dedicated revert-attribution event is a later PR. + if meta.err.is_some() { + tracing::debug!("transaction reverted, skipping"); + return (Vec::new(), false); + } + let instructions = relevant_instructions(tx, &self.settlement_program, &self.solflow_program); if instructions.is_empty() { - return; + return (Vec::new(), false); } // `relevant_instructions` reconstructs the account list internally to // resolve program ids; rebuild it once here so the decode can resolve // account indices to pubkeys too. - let account_keys = build_account_keys(tx); - let post_token_balances = tx - .meta - .as_ref() - .map(|meta| meta.post_token_balances.clone()) - .unwrap_or_default(); let ctx = TxContext { slot, signature, - account_keys, - post_token_balances, + account_keys: build_account_keys(tx), + post_token_balances: meta.post_token_balances.clone(), }; // `relevant_instructions` yields only settlement and SolFlow instructions. @@ -120,10 +162,7 @@ impl Decoder { // TODO: resolve order PDAs against persisted order rows once the store // adapter lands. Until then nothing resolves, so `SettlementFinalized` // events carry the tx-level fields with empty trades. - // TODO: skip transactions whose `meta.err` is set: a reverted settlement - // or order creation must not emit an event. Deferred until the persist - // path is wired (nothing is persisted yet). - let events = decode_settlement(&settlement, &ctx, |_order_pda| None); + let (events, decode_failed) = decode_settlement(&settlement, &ctx, |_order_pda| None); for instruction in instructions .iter() @@ -132,13 +171,10 @@ impl Decoder { self.decode_solflow(instruction); } - // TODO: persist `events` once the persistence adapter lands; for now the - // decode runs end to end but its output is dropped. - tracing::debug!( - slot = %ctx.slot, - event_count = events.len(), - "decoded settlement events" - ); + ( + events.into_iter().map(DecodedEvent::Settlement).collect(), + decode_failed, + ) } /// TODO: decode the SolFlow instruction data. The on-chain program does not @@ -163,7 +199,8 @@ struct ResolvedOrder { } /// Decode the settlement-program instructions of one transaction into domain -/// events. +/// events. The `bool` is true when at least one instruction failed to decode, +/// so the caller can dead-letter the transaction for replay. /// /// Pure: every tx-level input arrives through `ctx`, and any order field the /// `BeginSettle` wire does not carry is resolved through `resolve_order` (keyed @@ -173,23 +210,27 @@ fn decode_settlement( instructions: &[ResolvedInstruction], ctx: &TxContext, resolve_order: impl Fn(&Pubkey) -> Option, -) -> Vec { +) -> (Vec, bool) { let mut events = Vec::new(); + let mut decode_failed = false; // Instructions decodable on their own, without tx-level pairing. for instruction in instructions { let Ok((discriminator, _)) = recover_discriminator(&instruction.data) else { - tracing::debug!( + decode_failed = true; + // Warn, not debug: this dead-letters the transaction, so it needs to + // be findable in the logs alongside the row. + tracing::warn!( instruction_index = instruction.instruction_index, + err = %DecodeError::UnknownDiscriminator, "settlement instruction with an unknown discriminator, skipping" ); continue; }; // A landed (non-reverted) transaction carries valid instruction data, so // a decode failure here means a decoder bug or an unannounced program - // layout change, not a normal case. Surface it as a warning rather than - // dropping it silently. Once the persistence adapter lands these route to - // the dead-letter table. + // layout change, not a normal case. Surface it as a warning and set the + // failure flag so the transaction is dead-lettered. let decoded = match discriminator { SettlementInstruction::CreateOrder => { decode_order_created(instruction, &ctx.account_keys).map(|event| vec![event]) @@ -208,11 +249,14 @@ fn decode_settlement( }; match decoded { Ok(decoded_events) => events.extend(decoded_events), - Err(err) => tracing::warn!( - instruction_index = instruction.instruction_index, - %err, - "failed to decode settlement instruction" - ), + Err(err) => { + decode_failed = true; + tracing::warn!( + instruction_index = instruction.instruction_index, + %err, + "failed to decode settlement instruction" + ); + } } } @@ -222,8 +266,9 @@ fn decode_settlement( instructions, ctx, &resolve_order, + &mut decode_failed, )); - events + (events, decode_failed) } /// `CreateOrder` -> `OrderCreated`. The parser recovers the encoded order @@ -235,9 +280,9 @@ fn decode_order_created( ) -> Result { let mut accounts = instruction_account_keys(instruction, account_keys)?; let input = CreateOrderInput::parse(&instruction.data, &mut accounts) - .map_err(|_| DecodeError::SchemaMismatch)?; + .map_err(|err| DecodeError::SchemaMismatch(err.to_string()))?; let (intent, uid) = EncodedOrderIntent::decode_and_hash(&input.intent_bytes) - .map_err(|_| DecodeError::SchemaMismatch)?; + .map_err(|err| DecodeError::SchemaMismatch(err.to_string()))?; Ok(SettlementEvent::OrderCreated { order_uid: OrderUid(uid.to_bytes()), owner: to_sdk_pubkey(intent.owner), @@ -254,7 +299,7 @@ fn decode_buffers_created( ) -> Result, DecodeError> { let mut accounts = instruction_account_keys(instruction, account_keys)?; let input = CreateBufferInput::parse(&instruction.data, &mut accounts) - .map_err(|_| DecodeError::SchemaMismatch)?; + .map_err(|err| DecodeError::SchemaMismatch(err.to_string()))?; Ok(input .buffers .iter() @@ -268,11 +313,14 @@ fn decode_buffers_created( /// Pairing is by index: a parsed `BeginSettle` carries the top-level /// instruction index of its `FinalizeSettle` (`finalize_ix_index`), which must /// match a `FinalizeSettle` present in the same transaction. It is independent -/// of the two instructions' relative order. +/// of the two instructions' relative order. A pair that cannot be decoded (a +/// parse failure, unresolvable account keys, or a `BeginSettle` whose named +/// `FinalizeSettle` is missing) sets `decode_failed` and emits nothing. fn decode_settlements_finalized( instructions: &[ResolvedInstruction], ctx: &TxContext, resolve_order: &impl Fn(&Pubkey) -> Option, + decode_failed: &mut bool, ) -> Vec { // The solver is the transaction fee payer: the first account key, which // Solana guarantees is the signer that submitted the transaction. @@ -287,13 +335,23 @@ fn decode_settlements_finalized( }; let mut begin_accounts = match instruction_account_keys(begin, &ctx.account_keys) { Ok(accounts) => accounts, - Err(_) => continue, + Err(err) => { + *decode_failed = true; + tracing::warn!( + instruction_index = begin.instruction_index, + %err, + "BeginSettle account resolution failed, skipping" + ); + continue; + } }; let begin_input = match BeginSettleInput::parse(&begin.data, &mut begin_accounts) { Ok(input) => input, - Err(_) => { + Err(err) => { + *decode_failed = true; tracing::warn!( instruction_index = begin.instruction_index, + %err, "BeginSettle did not match the expected layout, skipping" ); continue; @@ -308,6 +366,7 @@ fn decode_settlements_finalized( Ok((SettlementInstruction::FinalizeSettle, _)) ) }) else { + *decode_failed = true; tracing::debug!( instruction_index = begin.instruction_index, finalize_ix_index = begin_input.finalize_ix_index, @@ -317,14 +376,24 @@ fn decode_settlements_finalized( }; let mut finalize_accounts = match instruction_account_keys(finalize, &ctx.account_keys) { Ok(accounts) => accounts, - Err(_) => continue, + Err(err) => { + *decode_failed = true; + tracing::warn!( + instruction_index = finalize.instruction_index, + %err, + "FinalizeSettle account resolution failed, skipping" + ); + continue; + } }; let finalize_input = match FinalizeSettleInput::parse(&finalize.data, &mut finalize_accounts) { Ok(input) => input, - Err(_) => { + Err(err) => { + *decode_failed = true; tracing::warn!( instruction_index = finalize.instruction_index, + %err, "FinalizeSettle did not match the expected layout, skipping" ); continue; @@ -363,9 +432,7 @@ fn decode_settlements_finalized( .collect(); events.push(SettlementEvent::SettlementFinalized { - // The wire carries `auction_id` as i64; it is non-negative in - // practice. - auction_id: begin_input.auction_id as u64, + auction_id: begin_input.auction_id, solver, tx_signature: ctx.signature, slot: ctx.slot, @@ -386,10 +453,12 @@ fn instruction_account_keys( .accounts .iter() .map(|&index| { - account_keys - .get(usize::from(index)) - .copied() - .ok_or(DecodeError::SchemaMismatch) + account_keys.get(usize::from(index)).copied().ok_or( + DecodeError::AccountIndexOutOfRange { + index, + len: account_keys.len(), + }, + ) }) .collect() } diff --git a/crates/solana-indexer/src/indexer/decoder/tests.rs b/crates/solana-indexer/src/indexer/decoder/tests.rs index 138ff6db28..d53581976c 100644 --- a/crates/solana-indexer/src/indexer/decoder/tests.rs +++ b/crates/solana-indexer/src/indexer/decoder/tests.rs @@ -5,7 +5,7 @@ use { types::{ Signature, channel::StreamUpdate, - events::{SettlementEvent, TradeDelta}, + events::{DecodedEvent, SettlementEvent, TradeDelta}, order::OrderUid, slot::Slot, tx::TxContext, @@ -16,6 +16,7 @@ use { Message, SubscribeUpdateTransactionInfo, Transaction, + TransactionError, TransactionStatusMeta, }, }, @@ -269,10 +270,11 @@ fn stream_tx(slot: Slot, signature: Signature, settlement: Pubkey) -> StreamUpda } } -/// Verifies the run loop drains buffered updates and returns Ok when the sender -/// drops. It does not assert the decoded event yet: decode output is dropped -/// until the persistence adapter lands, at which point this test should assert -/// the emitted event. +/// Verifies the run loop drains buffered updates and returns Ok when the +/// sender drops. Event content is not asserted here: the persistence bodies +/// are no-ops until the Postgres adapter lands, so nothing is observable +/// through them. Decode output is asserted directly in +/// `decode_wraps_settlement_events_as_decoded`. #[tokio::test] async fn run_drains_transactions_until_the_sender_drops() { let (settlement, solflow) = (pubkey(1), pubkey(2)); @@ -287,14 +289,13 @@ async fn run_drains_transactions_until_the_sender_drops() { assert!(decoder.run().await.is_ok()); } -/// A crafted `CreateOrder` decodes to `OrderCreated` with the UID (the -/// hash of the encoded intent), the intent's owner, and the `created_by` -/// account resolved from the instruction's account list. The account-list owner -/// differs from the intent owner, so this also pins that the event owner comes -/// from the intent data, not the accounts. -#[test] -fn create_order_decodes_to_order_created() { - let (settlement, solflow) = (pubkey(1), pubkey(2)); +/// Build a `CreateOrder` transaction fixture. Returns the tx, the expected +/// order UID (the hash of the encoded intent), and the `created_by` account. +/// The account-list owner (`pubkey(11)`) differs from the intent owner +/// (`[0x11; 32]`) so callers can pin that the event owner comes from the +/// intent data, not the accounts. +fn create_order_tx() -> (SubscribeUpdateTransactionInfo, OrderUid, Pubkey) { + let settlement = pubkey(1); let created_by = pubkey(12); // Account list: [settlement(0), owner(1), created_by(2), order_pda(3), // system(4)]. @@ -326,6 +327,100 @@ fn create_order_decodes_to_order_created() { vec![compiled(0, vec![1, 2, 3, 4], data)], vec![], ); + (tx, OrderUid(intent.uid().to_bytes()), created_by) +} + +/// A crafted `CreateOrder` decodes to `OrderCreated` with the UID (the +/// hash of the encoded intent), the intent's owner, and the `created_by` +/// account resolved from the instruction's account list. The account-list owner +/// differs from the intent owner, so this also pins that the event owner comes +/// from the intent data, not the accounts. +#[test] +fn create_order_decodes_to_order_created() { + let (settlement, solflow) = (pubkey(1), pubkey(2)); + let (tx, expected_uid, created_by) = create_order_tx(); + + let ctx = TxContext { + slot: Slot(5), + signature: signature(6), + account_keys: build_account_keys(&tx), + post_token_balances: vec![], + }; + let instructions = relevant_instructions(&tx, &settlement, &solflow); + let (events, decode_failed) = decode_settlement(&instructions, &ctx, |_| None); + + assert!(!decode_failed); + assert_eq!( + events, + vec![SettlementEvent::OrderCreated { + order_uid: expected_uid, + owner: Pubkey::new_from_array([0x11; 32]), + created_by, + }] + ); +} + +/// `decode` wraps settlement events as `DecodedEvent::Settlement` for `run` to +/// persist, and emits nothing once the same transaction carries `meta.err`: a +/// revert rolls back every account write, and it is not a decode failure to +/// dead-letter. One fixture for both, so `meta.err` is the only difference. +#[test] +fn decode_wraps_events_and_skips_reverted_transactions() { + let (settlement, solflow) = (pubkey(1), pubkey(2)); + let (mut tx, expected_uid, created_by) = create_order_tx(); + let (decoder, _sender) = test_decoder(settlement, solflow); + + let (events, decode_failed) = decoder.decode(&tx, Slot(5), signature(6)); + assert!(!decode_failed); + assert_eq!( + events, + vec![DecodedEvent::Settlement(SettlementEvent::OrderCreated { + order_uid: expected_uid, + owner: Pubkey::new_from_array([0x11; 32]), + created_by, + })] + ); + + tx.meta.as_mut().unwrap().err = Some(TransactionError { err: vec![1] }); + let (events, decode_failed) = decoder.decode(&tx, Slot(5), signature(6)); + assert!(!decode_failed); + assert_eq!(events, vec![]); +} + +/// A transaction update without `meta` carries no success flag, so it emits +/// nothing and is dead-lettered rather than decoded. The ingester forwards +/// updates on a valid body and signature alone, so the decoder cannot assume +/// `meta` is present. +#[test] +fn transaction_without_meta_is_dead_lettered() { + let (settlement, solflow) = (pubkey(1), pubkey(2)); + let (mut tx, ..) = create_order_tx(); + tx.meta = None; + let (decoder, _sender) = test_decoder(settlement, solflow); + + let (events, decode_failed) = decoder.decode(&tx, Slot(5), signature(6)); + + assert_eq!(events, vec![]); + assert!(decode_failed); +} + +/// A settlement instruction with an unknown discriminator sets the failure +/// flag and yields no event, while an instruction that decodes cleanly in the +/// same transaction still emits its event. +#[test] +fn unknown_discriminator_sets_failure_flag_and_keeps_good_events() { + let (settlement, solflow) = (pubkey(1), pubkey(2)); + let (mut tx, expected_uid, created_by) = create_order_tx(); + // Prepend a settlement instruction whose discriminator byte matches no + // known instruction. + tx.transaction + .as_mut() + .unwrap() + .message + .as_mut() + .unwrap() + .instructions + .insert(0, compiled(0, vec![1], vec![0xFF])); let ctx = TxContext { slot: Slot(5), @@ -334,18 +429,55 @@ fn create_order_decodes_to_order_created() { post_token_balances: vec![], }; let instructions = relevant_instructions(&tx, &settlement, &solflow); - let events = decode_settlement(&instructions, &ctx, |_| None); + let (events, decode_failed) = decode_settlement(&instructions, &ctx, |_| None); + assert!(decode_failed); assert_eq!( events, vec![SettlementEvent::OrderCreated { - order_uid: OrderUid(intent.uid().to_bytes()), + order_uid: expected_uid, owner: Pubkey::new_from_array([0x11; 32]), created_by, }] ); } +/// A `BeginSettle` whose named `FinalizeSettle` is not present in the +/// transaction emits no event and sets the failure flag. +#[test] +fn unpaired_begin_settle_sets_failure_flag() { + let (settlement, solflow) = (pubkey(1), pubkey(2)); + // Account list: [solver(0), settlement(1), sysvar(2), state(3), token(4)]. + let account_keys = vec![pubkey(10), settlement, pubkey(22), pubkey(23), pubkey(24)]; + + // BeginSettle body with zero orders, naming finalize index 1 while the tx + // has no instruction 1. + let mut begin_data = vec![SettlementInstruction::BeginSettle.discriminator()]; + begin_data.extend_from_slice(&1u16.to_le_bytes()); + begin_data.extend_from_slice(&4242i64.to_le_bytes()); + begin_data.push(0); + + let tx = tx_info( + account_keys, + vec![], + vec![], + vec![compiled(1, vec![2, 3, 4], begin_data)], + vec![], + ); + + let ctx = TxContext { + slot: Slot(5), + signature: signature(6), + account_keys: build_account_keys(&tx), + post_token_balances: vec![], + }; + let instructions = relevant_instructions(&tx, &settlement, &solflow); + let (events, decode_failed) = decode_settlement(&instructions, &ctx, |_| None); + + assert_eq!(events, vec![]); + assert!(decode_failed); +} + /// A crafted `BeginSettle` + `FinalizeSettle` pair decodes to one /// `SettlementFinalized`: the auction id read from the begin wire, the summed /// sell amount, the buy-side push amount paired to its order by position @@ -420,8 +552,9 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { post_token_balances: vec![], }; let instructions = relevant_instructions(&tx, &settlement, &solflow); - let events = decode_settlement(&instructions, &ctx, resolve_order); + let (events, decode_failed) = decode_settlement(&instructions, &ctx, resolve_order); + assert!(!decode_failed); assert_eq!( events, vec![SettlementEvent::SettlementFinalized { diff --git a/crates/solana-indexer/src/indexer/ingester.rs b/crates/solana-indexer/src/indexer/ingester.rs index a2253dbbbb..1fe1e26929 100644 --- a/crates/solana-indexer/src/indexer/ingester.rs +++ b/crates/solana-indexer/src/indexer/ingester.rs @@ -266,10 +266,12 @@ impl Ingester { settlement_program: Pubkey, solflow_program: Pubkey, ) -> Result<(), Error> { + // The proto field is a bare slot number, and `from_slot` is inclusive, so + // resume one past the last fully persisted slot. let from_slot = persistence .read_watermark() .await? - .map(|watermark| watermark + 1); + .map(|watermark| u64::from(watermark) + 1); let request = subscribe_request(settlement_program, solflow_program, from_slot); // The sink is the bidi request half: if kept, it can reconfigure the diff --git a/crates/solana-indexer/src/persistence.rs b/crates/solana-indexer/src/persistence.rs index adeeea95de..579c3bb723 100644 --- a/crates/solana-indexer/src/persistence.rs +++ b/crates/solana-indexer/src/persistence.rs @@ -3,6 +3,7 @@ use { crate::types::{ + Signature, commitment::{Commitment, UnfinalizedRow}, errors::PersistenceError, events::DecodedEvent, @@ -25,25 +26,47 @@ impl Persistence { pub(crate) async fn persist_events( &self, events: Vec, - new_watermark: u64, + new_watermark: Slot, ) -> Result<(), PersistenceError> { - todo!() + // No-op seam until the Postgres adapter lands. The adapter writes the + // events and advances the watermark in one SQL transaction: append rows + // as INSERT ON CONFLICT DO NOTHING, the watermark UPDATE guarded with + // WHERE slot < $new_watermark. + Ok(()) } /// Record a slot checkpoint. Rejects downward writes. - pub(crate) async fn write_watermark(&self, slot: u64) -> Result<(), PersistenceError> { - todo!() + pub(crate) async fn write_watermark(&self, slot: Slot) -> Result<(), PersistenceError> { + // No-op seam until the Postgres adapter lands, which adds the monotonic + // guard. + Ok(()) + } + + /// Record a transaction whose decode failed so recovery can replay it by + /// signature. One row per transaction. + /// + /// The row's `reason` column is not a parameter: a decoder error is the + /// only failure mode that reaches this table (spec ยง12.2), so the + /// adapter writes `'decoder_error'`. A second reason would arrive as a + /// typed argument. + pub(crate) async fn write_dead_letter( + &self, + signature: Signature, + slot: Slot, + ) -> Result<(), PersistenceError> { + // No-op seam until the Postgres adapter lands. + Ok(()) } /// Read persisted watermark for resuming after reconnect. - pub(crate) async fn read_watermark(&self) -> Result, PersistenceError> { + pub(crate) async fn read_watermark(&self) -> Result, PersistenceError> { todo!() } /// Record gaps that fell outside the replay window (write-only in v0.1). pub(crate) async fn record_lost_slot_range( &self, - range: RangeInclusive, + range: RangeInclusive, ) -> Result<(), PersistenceError> { todo!() } @@ -70,7 +93,7 @@ impl Persistence { /// (see `get_confirmed_rows`). pub(crate) async fn get_aged_rows( &self, - retention_horizon_slot: u64, + retention_horizon_slot: Slot, ) -> Result, PersistenceError> { todo!() } diff --git a/crates/solana-indexer/src/types/errors.rs b/crates/solana-indexer/src/types/errors.rs index a6559f4327..15eac9aff4 100644 --- a/crates/solana-indexer/src/types/errors.rs +++ b/crates/solana-indexer/src/types/errors.rs @@ -10,14 +10,17 @@ pub(crate) enum DecodeError { /// 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, + /// An account index did not resolve against the transaction's account list, + /// which includes the ALT (Address Lookup Table) loaded addresses. + #[error("account index {index} out of range for {len} account keys")] + AccountIndexOutOfRange { index: u8, len: usize }, /// The instruction was recognised but its schema did not match the on-chain - /// layout. - #[error("schema mismatch")] - SchemaMismatch, + /// layout. Carries the interface parser's error rendered as text, which + /// names the check that failed. Text rather than the `ProgramError` itself + /// because the interface does not re-export it and pins its own + /// `solana-program-error` major, and nothing branches on this. + #[error("schema mismatch: {0}")] + SchemaMismatch(String), } /// Failures surfaced from the persistence boundary. diff --git a/crates/solana-indexer/src/types/events.rs b/crates/solana-indexer/src/types/events.rs index fd56a66d3a..892bdfd775 100644 --- a/crates/solana-indexer/src/types/events.rs +++ b/crates/solana-indexer/src/types/events.rs @@ -55,7 +55,7 @@ pub(crate) enum SettlementEvent { /// A settlement was finalized on-chain. SettlementFinalized { /// Auction id this settlement belongs to. - auction_id: u64, + auction_id: i64, /// Solver that won the auction. solver: Pubkey, /// Transaction signature. diff --git a/crates/solana-indexer/src/types/recovery.rs b/crates/solana-indexer/src/types/recovery.rs index 59ba628623..748464d0c5 100644 --- a/crates/solana-indexer/src/types/recovery.rs +++ b/crates/solana-indexer/src/types/recovery.rs @@ -2,7 +2,10 @@ //! Recovery-flow types: PDA snapshots and the options struct for //! `getSignaturesForAddress` backfills. -use {crate::types::order::OrderUid, solana_sdk::pubkey::Pubkey}; +use { + crate::types::{order::OrderUid, slot::Slot}, + solana_sdk::pubkey::Pubkey, +}; /// Current on-chain snapshot of an order PDA, read by `getAccountInfo` for /// reconciliation. @@ -24,9 +27,9 @@ pub(crate) struct PdaSnapshot { #[derive(Debug, Clone, Default)] pub(crate) struct GetSignaturesOpts { /// Start slot (inclusive). `None` means "from the tip". - pub from_slot: Option, + pub from_slot: Option, /// End slot (inclusive). `None` means "to the tip". - pub to_slot: Option, + pub to_slot: Option, /// Cap on the number of signatures returned. pub limit: Option, /// Optional address filter (used when back-filling both programs diff --git a/crates/solana-indexer/src/types/wire.rs b/crates/solana-indexer/src/types/wire.rs index 36607ac212..84d5538619 100644 --- a/crates/solana-indexer/src/types/wire.rs +++ b/crates/solana-indexer/src/types/wire.rs @@ -26,6 +26,7 @@ pub use yellowstone_grpc_proto::{ Message, TokenBalance, Transaction, + TransactionError, TransactionStatusMeta, }, };