From 0eb421bbf7872bcb11c5d4c7b935e6786ae253d1 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 08:52:30 -0700 Subject: [PATCH 01/16] chore: extract constants into dedicated consts module --- src/consts.rs | 36 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/syncer.rs | 38 +++++--------------------------------- 3 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 src/consts.rs diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..0ba0dcc --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,36 @@ +//! Constants used throughout the DLP synchronization service. + +use crate::types::Pubkey; + +/// Size of a Solana public key in bytes. +pub(crate) const PUBKEY_LEN: usize = 32; + +/// Delegation program address. +pub(crate) const DELEGATION_PROGRAM: &str = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"; + +/// Delegation program pubkey in bytes. +pub(crate) const DELEGATION_PROGRAM_PUBKEY: &Pubkey = &[ + 181, 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, 53, 6, 235, 140, 229, + 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, +]; + +/// Size of a delegation record account in bytes. +pub(crate) const DELEGATION_RECORD_SIZE: u64 = 96; + +/// Instruction discriminator for undelegate operations. +pub(crate) const UNDELEGATE_DISCRIMINATOR: u8 = 3; + +/// Length of an instruction discriminator (Anchor programs). +pub(crate) const DISCRIMINATOR_LEN: usize = 8; + +/// Index of the delegation record account in undelegate instruction accounts. +pub(crate) const DELEGATION_RECORD_ACCOUNT_INDEX: usize = 6; + +/// Maximum pending subscription/unsubscription requests. +pub(crate) const MAX_PENDING_REQUESTS: usize = 256; + +/// Maximum pending account/transaction updates. +pub(crate) const MAX_PENDING_UPDATES: usize = 8192; + +/// Maximum reconnection attempts to the Laserstream. +pub(crate) const MAX_RECONNECT_ATTEMPTS: u32 = 16; diff --git a/src/lib.rs b/src/lib.rs index 29bb7df..4ab68c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ //! ``` mod channels; +mod consts; mod syncer; mod types; diff --git a/src/syncer.rs b/src/syncer.rs index aa8caf8..91a4424 100644 --- a/src/syncer.rs +++ b/src/syncer.rs @@ -22,41 +22,13 @@ use tokio::{ }; use crate::channels::DlpSyncChannelsInit; +use crate::consts::{ + DELEGATION_PROGRAM, DELEGATION_PROGRAM_PUBKEY, DELEGATION_RECORD_ACCOUNT_INDEX, + DELEGATION_RECORD_SIZE, DISCRIMINATOR_LEN, MAX_PENDING_REQUESTS, MAX_PENDING_UPDATES, + MAX_RECONNECT_ATTEMPTS, PUBKEY_LEN, UNDELEGATE_DISCRIMINATOR, +}; use crate::types::{AccountUpdate, DlpSyncError, Pubkey, Slot}; -/// Size of a Solana public key in bytes. -const PUBKEY_LEN: usize = 32; - -/// Delegation program address. -const DELEGATION_PROGRAM: &str = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"; - -/// Delegation program pubkey in bytes. -const DELEGATION_PROGRAM_PUBKEY: &Pubkey = &[ - 181, 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, 53, 6, 235, 140, 229, - 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, -]; - -/// Size of a delegation record account in bytes. -const DELEGATION_RECORD_SIZE: u64 = 96; - -/// Instruction discriminator for undelegate operations. -const UNDELEGATE_DISCRIMINATOR: u8 = 3; - -/// Length of an instruction discriminator (Anchor programs). -const DISCRIMINATOR_LEN: usize = 8; - -/// Index of the delegation record account in undelegate instruction accounts. -const DELEGATION_RECORD_ACCOUNT_INDEX: usize = 6; - -/// Maximum pending subscription/unsubscription requests. -const MAX_PENDING_REQUESTS: usize = 256; - -/// Maximum pending account/transaction updates. -const MAX_PENDING_UPDATES: usize = 8192; - -/// Maximum reconnection attempts to the Laserstream. -const MAX_RECONNECT_ATTEMPTS: u32 = 16; - /// Stream type alias for Laserstream updates. type LaserStream = Pin> + Send>>; From 014a3cc14355411df0f505cc30a72e0b46ed396b Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 08:55:24 -0700 Subject: [PATCH 02/16] chore: add transaction_syncer module with helper functions --- src/lib.rs | 1 + src/transaction_syncer.rs | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/transaction_syncer.rs diff --git a/src/lib.rs b/src/lib.rs index 4ab68c5..0c17716 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ mod channels; mod consts; mod syncer; +mod transaction_syncer; mod types; pub use channels::{DlpSyncChannelsInit, DlpSyncChannelsRequester}; diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs new file mode 100644 index 0000000..b6fbfed --- /dev/null +++ b/src/transaction_syncer.rs @@ -0,0 +1,67 @@ +//! Stateless helpers for processing undelegation transactions. + +#![allow(dead_code)] + +use std::collections::HashMap; + +use helius_laserstream::{ + grpc::{SubscribeRequestFilterTransactions, SubscribeUpdateTransaction}, + solana::storage::confirmed_block::CompiledInstruction, +}; + +use crate::consts::{ + DELEGATION_PROGRAM, DELEGATION_PROGRAM_PUBKEY, DELEGATION_RECORD_ACCOUNT_INDEX, + DISCRIMINATOR_LEN, UNDELEGATE_DISCRIMINATOR, +}; +use crate::types::Pubkey; + +/// Creates the subscription filter for undelegation transactions. +/// +/// Returns a HashMap to be merged into the SubscribeRequest's transactions field. +pub fn create_filter() -> HashMap { + let mut transactions = HashMap::new(); + let tx_filter = SubscribeRequestFilterTransactions { + account_include: vec![DELEGATION_PROGRAM.into()], + ..Default::default() + }; + transactions.insert("undelegations".into(), tx_filter); + transactions +} + +/// Processes a transaction update and extracts undelegated pubkeys. +/// +/// Returns a Vec of (record_pubkey, slot) tuples for each undelegation found. +pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec<(Pubkey, u64)> { + let mut undelegations = Vec::new(); + + let Some(message) = txn + .transaction + .as_ref() + .and_then(|t| t.transaction.as_ref().zip(t.meta.as_ref())) + .and_then(|(t, m)| m.err.is_none().then_some(t.message.as_ref()).flatten()) + else { + return undelegations; + }; + + let accounts = &message.account_keys; + + let is_undelegate = |ix: &CompiledInstruction| { + let program_id = accounts.get(ix.program_id_index as usize)?; + (program_id == DELEGATION_PROGRAM_PUBKEY).then_some(())?; + + let (discriminator, _) = ix.data.split_at_checked(DISCRIMINATOR_LEN)?; + (discriminator[0] == UNDELEGATE_DISCRIMINATOR).then_some(())?; + + ix.accounts + .get(DELEGATION_RECORD_ACCOUNT_INDEX) + .and_then(|&idx| accounts.get(idx as usize)) + }; + + for record_bytes in message.instructions.iter().filter_map(is_undelegate) { + if let Ok(record) = Pubkey::try_from(record_bytes.as_slice()) { + undelegations.push((record, txn.slot)); + } + } + + undelegations +} From 7ebf7c8b9142b6ce40c3a76f4b76467965e421bc Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 08:59:29 -0700 Subject: [PATCH 03/16] refactor: integrate transaction_syncer into syncer --- src/syncer.rs | 51 ++++++--------------------------------- src/transaction_syncer.rs | 2 -- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/src/syncer.rs b/src/syncer.rs index 91a4424..2b7e38a 100644 --- a/src/syncer.rs +++ b/src/syncer.rs @@ -10,10 +10,8 @@ use helius_laserstream::{ grpc::{ subscribe_request_filter_accounts_filter::Filter, subscribe_update::UpdateOneof, SubscribeRequest, SubscribeRequestFilterAccounts, SubscribeRequestFilterAccountsFilter, - SubscribeRequestFilterTransactions, SubscribeRequestPing, SubscribeUpdate, - SubscribeUpdateAccount, SubscribeUpdateTransaction, + SubscribeRequestPing, SubscribeUpdate, SubscribeUpdateAccount, SubscribeUpdateTransaction, }, - solana::storage::confirmed_block::CompiledInstruction, LaserstreamConfig, LaserstreamError, }; use tokio::{ @@ -23,10 +21,10 @@ use tokio::{ use crate::channels::DlpSyncChannelsInit; use crate::consts::{ - DELEGATION_PROGRAM, DELEGATION_PROGRAM_PUBKEY, DELEGATION_RECORD_ACCOUNT_INDEX, - DELEGATION_RECORD_SIZE, DISCRIMINATOR_LEN, MAX_PENDING_REQUESTS, MAX_PENDING_UPDATES, - MAX_RECONNECT_ATTEMPTS, PUBKEY_LEN, UNDELEGATE_DISCRIMINATOR, + DELEGATION_PROGRAM, DELEGATION_RECORD_SIZE, MAX_PENDING_REQUESTS, MAX_PENDING_UPDATES, + MAX_RECONNECT_ATTEMPTS, PUBKEY_LEN, }; +use crate::transaction_syncer; use crate::types::{AccountUpdate, DlpSyncError, Pubkey, Slot}; /// Stream type alias for Laserstream updates. @@ -189,38 +187,8 @@ impl DlpSyncer { /// Handles a transaction update, extracting undelegations. fn handle_transaction_update(&self, txn: SubscribeUpdateTransaction) { - let Some(message) = txn - .transaction - .and_then(|t| t.transaction.zip(t.meta)) - .and_then(|(t, m)| m.err.is_none().then_some(t.message)) - .flatten() - else { - return; - }; - - let accounts = &message.account_keys; - - let is_undelegate = |ix: &CompiledInstruction| { - let program_id = accounts.get(ix.program_id_index as usize)?; - (program_id == DELEGATION_PROGRAM_PUBKEY).then_some(())?; - - let (discriminator, _) = ix.data.split_at_checked(DISCRIMINATOR_LEN)?; - (discriminator[0] == UNDELEGATE_DISCRIMINATOR).then_some(())?; - - ix.accounts - .get(DELEGATION_RECORD_ACCOUNT_INDEX) - .and_then(|&idx| accounts.get(idx as usize)) - }; - - for record_bytes in message.instructions.iter().filter_map(is_undelegate) { - let Ok(record) = Pubkey::try_from(record_bytes.as_slice()) else { - continue; - }; - - let update = AccountUpdate::Undelegated { - record, - slot: txn.slot, - }; + for (record, slot) in transaction_syncer::process_update(&txn) { + let update = AccountUpdate::Undelegated { record, slot }; if let Err(error) = self.updates.try_send(update) { tracing::error!(%error, "failed to send undelegation update"); @@ -237,7 +205,6 @@ impl DlpSyncer { async fn connect(config: LaserstreamConfig) -> Result { let mut accounts = HashMap::new(); let mut slots = HashMap::new(); - let mut transactions = HashMap::new(); // Subscribe to delegation record accounts let account_filter = SubscribeRequestFilterAccounts { @@ -250,11 +217,7 @@ impl DlpSyncer { accounts.insert("delegations".into(), account_filter); // Subscribe to undelegation transactions - let tx_filter = SubscribeRequestFilterTransactions { - account_include: vec![DELEGATION_PROGRAM.into()], - ..Default::default() - }; - transactions.insert("undelegations".into(), tx_filter); + let transactions = transaction_syncer::create_filter(); // Subscribe to all slot updates slots.insert("slots".into(), Default::default()); diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs index b6fbfed..af362df 100644 --- a/src/transaction_syncer.rs +++ b/src/transaction_syncer.rs @@ -1,7 +1,5 @@ //! Stateless helpers for processing undelegation transactions. -#![allow(dead_code)] - use std::collections::HashMap; use helius_laserstream::{ From cdc54d6d8783ff5ccc0e810558475bbfc4a44c2d Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 09:05:40 -0700 Subject: [PATCH 04/16] chore: public export of transaction syncer module --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0c17716..4d88c47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ mod channels; mod consts; mod syncer; -mod transaction_syncer; +pub mod transaction_syncer; mod types; pub use channels::{DlpSyncChannelsInit, DlpSyncChannelsRequester}; From 2f2f7563c4d169e7980a8c0e1c79462b5b03784b Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 09:16:06 -0700 Subject: [PATCH 05/16] chore: add transaction syncer process tests --- src/transaction_syncer.rs | 170 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs index af362df..b14a8c8 100644 --- a/src/transaction_syncer.rs +++ b/src/transaction_syncer.rs @@ -63,3 +63,173 @@ pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec<(Pubkey, u64)> { undelegations } + +#[cfg(test)] +mod tests { + use super::*; + use helius_laserstream::{ + grpc::SubscribeUpdateTransactionInfo, + solana::storage::confirmed_block::{Message, Transaction, TransactionStatusMeta}, + }; + + fn create_undelegate_instruction( + program_id_index: u32, + record_account_index: u8, + ) -> CompiledInstruction { + let mut data = vec![0u8; DISCRIMINATOR_LEN]; + data[0] = UNDELEGATE_DISCRIMINATOR; + let mut accounts = vec![0u8; DELEGATION_RECORD_ACCOUNT_INDEX + 1]; + accounts[DELEGATION_RECORD_ACCOUNT_INDEX] = record_account_index; + CompiledInstruction { + program_id_index, + accounts, + data, + } + } + + fn create_other_instruction(program_id_index: u32) -> CompiledInstruction { + CompiledInstruction { + program_id_index, + accounts: vec![0, 1, 2], + data: vec![0u8; DISCRIMINATOR_LEN], + } + } + + fn create_txn_update( + slot: u64, + account_keys: Vec>, + instructions: Vec, + err: Option, + ) -> SubscribeUpdateTransaction { + SubscribeUpdateTransaction { + slot, + transaction: Some(SubscribeUpdateTransactionInfo { + signature: vec![], + is_vote: false, + transaction: Some(Transaction { + signatures: vec![], + message: Some(Message { + header: None, + account_keys, + recent_blockhash: vec![], + instructions, + versioned: false, + address_table_lookups: vec![], + }), + }), + meta: Some(TransactionStatusMeta { + err, + ..Default::default() + }), + index: 0, + }), + } + } + + fn random_pubkey() -> Vec { + (0..32).map(|i| i as u8).collect() + } + + fn record_pubkey(seed: u8) -> Vec { + vec![seed; 32] + } + + #[test] + fn test_process_update_with_undelegation_instruction() { + let delegation_program = DELEGATION_PROGRAM_PUBKEY.to_vec(); + let record = record_pubkey(42); + let account_keys = vec![random_pubkey(), delegation_program, record.clone()]; + let instructions = vec![create_undelegate_instruction(1, 2)]; + let txn = create_txn_update(100, account_keys, instructions, None); + + let result = process_update(&txn); + + assert_eq!(result.len(), 1); + let expected_record: Pubkey = record.as_slice().try_into().unwrap(); + assert_eq!(result[0], (expected_record, 100)); + } + + #[test] + fn test_process_update_without_undelegation_instruction() { + let other_program = random_pubkey(); + let account_keys = vec![random_pubkey(), other_program]; + let instructions = vec![create_other_instruction(1)]; + let txn = create_txn_update(100, account_keys, instructions, None); + + let result = process_update(&txn); + + assert!(result.is_empty()); + } + + #[test] + fn test_process_update_with_malformed_data() { + let delegation_program = DELEGATION_PROGRAM_PUBKEY.to_vec(); + let account_keys = vec![random_pubkey(), delegation_program]; + let malformed_ix = CompiledInstruction { + program_id_index: 1, + accounts: vec![], + data: vec![], + }; + let txn = create_txn_update(100, account_keys, vec![malformed_ix], None); + + let result = process_update(&txn); + + assert!(result.is_empty()); + } + + #[test] + fn test_process_update_with_multiple_undelegation_instructions() { + let delegation_program = DELEGATION_PROGRAM_PUBKEY.to_vec(); + let record1 = record_pubkey(1); + let record2 = record_pubkey(2); + let record3 = record_pubkey(3); + let account_keys = vec![ + random_pubkey(), + delegation_program, + record1.clone(), + record2.clone(), + record3.clone(), + ]; + let instructions = vec![ + create_undelegate_instruction(1, 2), + create_undelegate_instruction(1, 3), + create_undelegate_instruction(1, 4), + ]; + let txn = create_txn_update(200, account_keys, instructions, None); + + let result = process_update(&txn); + + assert_eq!(result.len(), 3); + let expected1: Pubkey = record1.as_slice().try_into().unwrap(); + let expected2: Pubkey = record2.as_slice().try_into().unwrap(); + let expected3: Pubkey = record3.as_slice().try_into().unwrap(); + assert_eq!(result[0], (expected1, 200)); + assert_eq!(result[1], (expected2, 200)); + assert_eq!(result[2], (expected3, 200)); + } + + #[test] + fn test_process_update_with_mixed_instructions() { + let delegation_program = DELEGATION_PROGRAM_PUBKEY.to_vec(); + let other_program = random_pubkey(); + let record = record_pubkey(99); + let account_keys = vec![ + random_pubkey(), + delegation_program, + other_program, + record.clone(), + ]; + let instructions = vec![ + create_other_instruction(2), + create_undelegate_instruction(1, 3), + create_other_instruction(2), + ]; + let txn = create_txn_update(300, account_keys, instructions, None); + + let result = process_update(&txn); + + assert_eq!(result.len(), 1); + let expected_record: Pubkey = record.as_slice().try_into().unwrap(); + assert_eq!(result[0], (expected_record, 300)); + } +} From 110392eeeabe3463ccc476bf7361e0b9ff8e8834 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 09:18:03 -0700 Subject: [PATCH 06/16] chore: add CI workflow --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0d10d57 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +on: + push: + branches: [master, dev] + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + +name: CI + +jobs: + ci: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + override: true + + - name: Setup Rust nightly for formatting + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: rustfmt + override: false + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Run tests + run: cargo test + + - name: Check formatting + run: cargo +nightly fmt --check + + - name: Run clippy + run: cargo clippy --all-targets -- -D warnings From ae1160b74a232c47341a27b7a7a67f45515eda8d Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 09:21:15 -0700 Subject: [PATCH 07/16] chore: test that delegation program pubkey representations match --- Cargo.toml | 3 +++ src/consts.rs | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ce9bac7..98bf18e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,6 @@ futures = "0.3" helius-laserstream = "0.1.5" tokio = { version = "1.0", features = ["sync", "macros"] } tracing = "0.1" + +[dev-dependencies] +bs58 = "0.5.1" diff --git a/src/consts.rs b/src/consts.rs index 0ba0dcc..55f41df 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -34,3 +34,20 @@ pub(crate) const MAX_PENDING_UPDATES: usize = 8192; /// Maximum reconnection attempts to the Laserstream. pub(crate) const MAX_RECONNECT_ATTEMPTS: u32 = 16; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn delegation_program_pubkey_matches_string() { + let decoded = bs58::decode(DELEGATION_PROGRAM) + .into_vec() + .expect("valid base58"); + assert_eq!( + decoded.as_slice(), + DELEGATION_PROGRAM_PUBKEY, + "DELEGATION_PROGRAM base58 string does not match DELEGATION_PROGRAM_PUBKEY bytes" + ); + } +} From 809345c44c709f4a9689939d1f2f5f35909d6372 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 09:23:55 -0700 Subject: [PATCH 08/16] chore: ensure we don't run the docs as tests --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4d88c47..a5f6d6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! //! # Usage //! -//! ```no_run +//! ```ignore //! use dlp_sync::DlpSyncer; //! //! # async fn example() -> Result<(), dlp_sync::DlpSyncError> { From 7d94f7a097b990413a3669237a98350fae6caefc Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 20 Jan 2026 17:53:30 -0700 Subject: [PATCH 09/16] chore: don't duplicate same transaction slot --- src/syncer.rs | 3 ++- src/transaction_syncer.rs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/syncer.rs b/src/syncer.rs index 2b7e38a..21b8af5 100644 --- a/src/syncer.rs +++ b/src/syncer.rs @@ -187,7 +187,8 @@ impl DlpSyncer { /// Handles a transaction update, extracting undelegations. fn handle_transaction_update(&self, txn: SubscribeUpdateTransaction) { - for (record, slot) in transaction_syncer::process_update(&txn) { + let slot = txn.slot; + for record in transaction_syncer::process_update(&txn) { let update = AccountUpdate::Undelegated { record, slot }; if let Err(error) = self.updates.try_send(update) { diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs index b14a8c8..001b600 100644 --- a/src/transaction_syncer.rs +++ b/src/transaction_syncer.rs @@ -28,8 +28,8 @@ pub fn create_filter() -> HashMap { /// Processes a transaction update and extracts undelegated pubkeys. /// -/// Returns a Vec of (record_pubkey, slot) tuples for each undelegation found. -pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec<(Pubkey, u64)> { +/// Returns a Vec of record_pubkeys for each undelegation found. +pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec { let mut undelegations = Vec::new(); let Some(message) = txn @@ -57,7 +57,7 @@ pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec<(Pubkey, u64)> { for record_bytes in message.instructions.iter().filter_map(is_undelegate) { if let Ok(record) = Pubkey::try_from(record_bytes.as_slice()) { - undelegations.push((record, txn.slot)); + undelegations.push(record); } } @@ -146,7 +146,7 @@ mod tests { assert_eq!(result.len(), 1); let expected_record: Pubkey = record.as_slice().try_into().unwrap(); - assert_eq!(result[0], (expected_record, 100)); + assert_eq!(result[0], expected_record); } #[test] @@ -203,9 +203,9 @@ mod tests { let expected1: Pubkey = record1.as_slice().try_into().unwrap(); let expected2: Pubkey = record2.as_slice().try_into().unwrap(); let expected3: Pubkey = record3.as_slice().try_into().unwrap(); - assert_eq!(result[0], (expected1, 200)); - assert_eq!(result[1], (expected2, 200)); - assert_eq!(result[2], (expected3, 200)); + assert_eq!(result[0], expected1); + assert_eq!(result[1], expected2); + assert_eq!(result[2], expected3); } #[test] @@ -230,6 +230,6 @@ mod tests { assert_eq!(result.len(), 1); let expected_record: Pubkey = record.as_slice().try_into().unwrap(); - assert_eq!(result[0], (expected_record, 300)); + assert_eq!(result[0], expected_record); } } From d1b0d9d097529b98841c5e39c518a2522e012df4 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 08:14:58 -0700 Subject: [PATCH 10/16] chore: apply code suggestions Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Babur Makhmudov <31780624+bmuddha@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- src/consts.rs | 5 +---- src/transaction_syncer.rs | 19 ++++++++----------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d10d57..77a8299 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,19 +29,19 @@ jobs: override: false - name: Cache cargo registry - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo index - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/git key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} diff --git a/src/consts.rs b/src/consts.rs index 55f41df..b764103 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -9,10 +9,7 @@ pub(crate) const PUBKEY_LEN: usize = 32; pub(crate) const DELEGATION_PROGRAM: &str = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"; /// Delegation program pubkey in bytes. -pub(crate) const DELEGATION_PROGRAM_PUBKEY: &Pubkey = &[ - 181, 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, 53, 6, 235, 140, 229, - 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, -]; +const DELEGATION_PROGRAM_PUBKEY: Pubkey = bs58::decode(DELEGATION_PROGRAM.as_bytes()).into_array_const_unwrap(); /// Size of a delegation record account in bytes. pub(crate) const DELEGATION_RECORD_SIZE: u64 = 96; diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs index 001b600..ed8d9bf 100644 --- a/src/transaction_syncer.rs +++ b/src/transaction_syncer.rs @@ -29,16 +29,14 @@ pub fn create_filter() -> HashMap { /// Processes a transaction update and extracts undelegated pubkeys. /// /// Returns a Vec of record_pubkeys for each undelegation found. -pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec { - let mut undelegations = Vec::new(); - +pub fn process_update(txn: &SubscribeUpdateTransaction) -> impl Iterator + '_ { let Some(message) = txn .transaction .as_ref() .and_then(|t| t.transaction.as_ref().zip(t.meta.as_ref())) .and_then(|(t, m)| m.err.is_none().then_some(t.message.as_ref()).flatten()) else { - return undelegations; + return Either::Left(std::iter::empty()); }; let accounts = &message.account_keys; @@ -55,13 +53,12 @@ pub fn process_update(txn: &SubscribeUpdateTransaction) -> Vec { .and_then(|&idx| accounts.get(idx as usize)) }; - for record_bytes in message.instructions.iter().filter_map(is_undelegate) { - if let Ok(record) = Pubkey::try_from(record_bytes.as_slice()) { - undelegations.push(record); - } - } - - undelegations + let undelegations = message + .instructions + .iter() + .filter_map(is_undelegate) + .filter_map(|bytes| Pubkey::try_from(bytes.as_slice()).ok()); + Either::Right(undelegations) } #[cfg(test)] From 4d9dbf833da9ea79a24253a8ee46b3e7e933db37 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 08:22:03 -0700 Subject: [PATCH 11/16] chore: cleanup build after suggested changes --- Cargo.toml | 5 ++--- src/consts.rs | 20 ++------------------ src/transaction_syncer.rs | 13 +++++++------ 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 98bf18e..7dfe04e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,9 @@ description = "Real-time synchronization of magicblock delegation records" readme = "README.md" [dependencies] +bs58 = "0.5.1" +either = "1.15.0" futures = "0.3" helius-laserstream = "0.1.5" tokio = { version = "1.0", features = ["sync", "macros"] } tracing = "0.1" - -[dev-dependencies] -bs58 = "0.5.1" diff --git a/src/consts.rs b/src/consts.rs index b764103..1e75036 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -9,7 +9,8 @@ pub(crate) const PUBKEY_LEN: usize = 32; pub(crate) const DELEGATION_PROGRAM: &str = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"; /// Delegation program pubkey in bytes. -const DELEGATION_PROGRAM_PUBKEY: Pubkey = bs58::decode(DELEGATION_PROGRAM.as_bytes()).into_array_const_unwrap(); +pub(crate) const DELEGATION_PROGRAM_PUBKEY: Pubkey = + bs58::decode(DELEGATION_PROGRAM.as_bytes()).into_array_const_unwrap(); /// Size of a delegation record account in bytes. pub(crate) const DELEGATION_RECORD_SIZE: u64 = 96; @@ -31,20 +32,3 @@ pub(crate) const MAX_PENDING_UPDATES: usize = 8192; /// Maximum reconnection attempts to the Laserstream. pub(crate) const MAX_RECONNECT_ATTEMPTS: u32 = 16; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn delegation_program_pubkey_matches_string() { - let decoded = bs58::decode(DELEGATION_PROGRAM) - .into_vec() - .expect("valid base58"); - assert_eq!( - decoded.as_slice(), - DELEGATION_PROGRAM_PUBKEY, - "DELEGATION_PROGRAM base58 string does not match DELEGATION_PROGRAM_PUBKEY bytes" - ); - } -} diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs index ed8d9bf..866bac3 100644 --- a/src/transaction_syncer.rs +++ b/src/transaction_syncer.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use either::Either; use helius_laserstream::{ grpc::{SubscribeRequestFilterTransactions, SubscribeUpdateTransaction}, solana::storage::confirmed_block::CompiledInstruction, @@ -43,7 +44,7 @@ pub fn process_update(txn: &SubscribeUpdateTransaction) -> impl Iterator>(); assert_eq!(result.len(), 1); let expected_record: Pubkey = record.as_slice().try_into().unwrap(); @@ -153,7 +154,7 @@ mod tests { let instructions = vec![create_other_instruction(1)]; let txn = create_txn_update(100, account_keys, instructions, None); - let result = process_update(&txn); + let result = process_update(&txn).collect::>(); assert!(result.is_empty()); } @@ -169,7 +170,7 @@ mod tests { }; let txn = create_txn_update(100, account_keys, vec![malformed_ix], None); - let result = process_update(&txn); + let result = process_update(&txn).collect::>(); assert!(result.is_empty()); } @@ -194,7 +195,7 @@ mod tests { ]; let txn = create_txn_update(200, account_keys, instructions, None); - let result = process_update(&txn); + let result = process_update(&txn).collect::>(); assert_eq!(result.len(), 3); let expected1: Pubkey = record1.as_slice().try_into().unwrap(); @@ -223,7 +224,7 @@ mod tests { ]; let txn = create_txn_update(300, account_keys, instructions, None); - let result = process_update(&txn); + let result = process_update(&txn).collect::>(); assert_eq!(result.len(), 1); let expected_record: Pubkey = record.as_slice().try_into().unwrap(); From 00d6fece6270d7a06746a9e220d41df5f782af3f Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 08:24:43 -0700 Subject: [PATCH 12/16] chore: upgrade to latest rust actions per coderabbit --- .github/workflows/ci.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77a8299..b1241bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,19 +14,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Rust - uses: actions-rs/toolchain@v1 + - name: Setup Rust stable + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: rustfmt, clippy - override: true - name: Setup Rust nightly for formatting - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly components: rustfmt - override: false - name: Cache cargo registry uses: actions/cache@v4 From 2264246374106be7b415e9ab4e592f89ef307d79 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 08:29:39 -0700 Subject: [PATCH 13/16] chore: fix CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1241bb..55d0cc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Rust nightly for formatting uses: dtolnay/rust-toolchain@nightly with: - components: rustfmt + components: rustfmt, clippy - name: Cache cargo registry uses: actions/cache@v4 From 40aafc92bb894084499a4b62625442edcfbe2084 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 08:33:09 -0700 Subject: [PATCH 14/16] chore: run ci only while PRing --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55d0cc4..2a55c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,4 @@ on: - push: - branches: [master, dev] pull_request: types: [opened, reopened, synchronize, ready_for_review] @@ -8,7 +6,6 @@ name: CI jobs: ci: - if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Checkout code From bca4068853dbce5ea8c61b87e0f1cc1117ff06cf Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 17:33:21 -0700 Subject: [PATCH 15/16] chore: adding _real_ undelegation transaction test --- src/transaction_syncer.rs | 282 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/src/transaction_syncer.rs b/src/transaction_syncer.rs index 866bac3..ff486f1 100644 --- a/src/transaction_syncer.rs +++ b/src/transaction_syncer.rs @@ -230,4 +230,286 @@ mod tests { let expected_record: Pubkey = record.as_slice().try_into().unwrap(); assert_eq!(result[0], expected_record); } + + #[test] + fn test_process_update_real_undelegate_transaction() { + use helius_laserstream::solana::storage::confirmed_block::{ + InnerInstruction, InnerInstructions, MessageHeader, + }; + + let account_keys: Vec> = vec![ + vec![ + 5, 62, 162, 42, 88, 56, 179, 161, 33, 20, 15, 2, 170, 67, 59, 11, 147, 146, 74, + 122, 186, 28, 135, 70, 6, 17, 193, 187, 246, 101, 67, 254, + ], + vec![ + 2, 72, 123, 149, 154, 28, 42, 5, 203, 133, 136, 151, 147, 6, 51, 17, 210, 14, 230, + 70, 5, 92, 116, 249, 173, 225, 5, 243, 123, 218, 107, 121, + ], + vec![ + 2, 156, 240, 31, 8, 227, 71, 24, 38, 134, 176, 55, 100, 79, 131, 141, 136, 167, + 250, 23, 143, 90, 223, 105, 3, 218, 146, 242, 186, 116, 159, 245, + ], + vec![ + 16, 244, 61, 162, 137, 30, 7, 140, 196, 156, 106, 131, 37, 154, 52, 54, 38, 7, 200, + 134, 91, 126, 171, 131, 76, 71, 53, 113, 224, 191, 220, 131, + ], + vec![ + 93, 185, 64, 160, 51, 203, 91, 169, 63, 16, 190, 250, 6, 127, 50, 34, 83, 69, 202, + 36, 207, 2, 26, 127, 73, 14, 64, 42, 168, 112, 243, 247, + ], + vec![ + 95, 232, 166, 5, 73, 185, 224, 5, 43, 125, 32, 58, 120, 165, 19, 29, 204, 186, 36, + 171, 140, 16, 143, 110, 129, 187, 102, 247, 116, 214, 217, 104, + ], + vec![ + 191, 216, 249, 0, 219, 24, 217, 241, 219, 105, 68, 147, 70, 243, 61, 204, 216, 198, + 228, 218, 222, 249, 42, 129, 249, 80, 136, 232, 65, 137, 1, 40, + ], + vec![ + 206, 188, 216, 84, 252, 33, 46, 58, 47, 194, 4, 232, 107, 53, 162, 90, 194, 1, 254, + 180, 229, 235, 81, 166, 83, 235, 160, 220, 42, 36, 142, 24, + ], + vec![ + 235, 210, 196, 10, 117, 2, 125, 28, 175, 161, 36, 233, 191, 80, 162, 45, 13, 87, + 204, 254, 249, 45, 4, 175, 236, 3, 249, 6, 117, 220, 24, 8, + ], + vec![ + 244, 48, 161, 207, 180, 238, 8, 71, 42, 115, 120, 201, 130, 115, 52, 119, 188, 114, + 14, 25, 63, 36, 97, 28, 199, 173, 180, 46, 190, 13, 27, 108, + ], + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ], + vec![ + 3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, + 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0, + ], + vec![ + 162, 211, 1, 190, 154, 210, 30, 64, 120, 186, 237, 91, 247, 216, 155, 174, 121, + 251, 243, 100, 155, 28, 35, 33, 25, 86, 113, 10, 107, 4, 182, 135, + ], + vec![ + 181, 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, 53, 6, + 235, 140, 229, 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, + ], + vec![ + 185, 255, 31, 113, 29, 105, 39, 129, 63, 161, 34, 161, 231, 204, 45, 54, 62, 248, + 189, 69, 172, 212, 222, 101, 51, 136, 202, 69, 225, 212, 233, 238, + ], + ]; + + let instructions = vec![ + CompiledInstruction { + program_id_index: 11, + accounts: vec![], + data: vec![2, 80, 52, 3, 0], + }, + CompiledInstruction { + program_id_index: 11, + accounts: vec![], + data: vec![3, 128, 56, 1, 0, 0, 0, 0, 0], + }, + CompiledInstruction { + program_id_index: 11, + accounts: vec![], + data: vec![4, 32, 142, 85, 0], + }, + CompiledInstruction { + program_id_index: 13, + accounts: vec![0, 5, 2, 7, 8, 6, 1, 14, 10], + data: vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 75, 15, 0, 0, 0, 0, 0, 1, + 16, 0, 0, 0, 255, 176, 4, 245, 188, 253, 124, 25, 208, 12, 0, 0, 0, 0, 0, 0, + ], + }, + CompiledInstruction { + program_id_index: 13, + accounts: vec![0, 5, 2, 7, 8, 6, 1, 10], + data: vec![2, 0, 0, 0, 0, 0, 0, 0], + }, + CompiledInstruction { + program_id_index: 13, + accounts: vec![0, 5, 12, 9, 2, 7, 8, 6, 3, 4, 1, 10], + data: vec![3, 0, 0, 0, 0, 0, 0, 0], + }, + ]; + + let inner_instructions = vec![ + InnerInstructions { + index: 3, + instructions: vec![ + InnerInstruction { + program_id_index: 10, + accounts: vec![0, 2], + data: vec![ + 0, 0, 0, 0, 0, 75, 15, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 181, + 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, + 53, 6, 235, 140, 229, 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, + ], + stack_height: Some(2), + }, + InnerInstruction { + program_id_index: 10, + accounts: vec![0, 7], + data: vec![ + 0, 0, 0, 0, 128, 240, 22, 0, 0, 0, 0, 0, 88, 0, 0, 0, 0, 0, 0, 0, 181, + 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, + 53, 6, 235, 140, 229, 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, + ], + stack_height: Some(2), + }, + ], + }, + InnerInstructions { + index: 5, + instructions: vec![ + InnerInstruction { + program_id_index: 10, + accounts: vec![0, 9], + data: vec![ + 0, 0, 0, 0, 0, 75, 15, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 181, + 183, 0, 225, 242, 87, 58, 192, 204, 6, 34, 1, 52, 74, 207, 151, 184, + 53, 6, 235, 140, 229, 25, 152, 204, 98, 126, 24, 147, 128, 167, 62, + ], + stack_height: Some(2), + }, + InnerInstruction { + program_id_index: 12, + accounts: vec![5, 9, 0, 10], + data: vec![ + 196, 28, 41, 206, 48, 37, 51, 167, 2, 0, 0, 0, 7, 0, 0, 0, 99, 111, + 117, 110, 116, 101, 114, 32, 0, 0, 0, 5, 62, 162, 42, 88, 56, 179, 161, + 33, 20, 15, 2, 170, 67, 59, 11, 147, 146, 74, 122, 186, 28, 135, 70, 6, + 17, 193, 187, 246, 101, 67, 254, + ], + stack_height: Some(2), + }, + InnerInstruction { + program_id_index: 10, + accounts: vec![0, 5], + data: vec![ + 0, 0, 0, 0, 0, 75, 15, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 162, + 211, 1, 190, 154, 210, 30, 64, 120, 186, 237, 91, 247, 216, 155, 174, + 121, 251, 243, 100, 155, 28, 35, 33, 25, 86, 113, 10, 107, 4, 182, 135, + ], + stack_height: Some(3), + }, + InnerInstruction { + program_id_index: 10, + accounts: vec![0, 5], + data: vec![2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + stack_height: Some(2), + }, + ], + }, + ]; + + let txn = SubscribeUpdateTransaction { + slot: 436772071, + transaction: Some(SubscribeUpdateTransactionInfo { + signature: vec![ + 22, 112, 23, 161, 121, 241, 200, 28, 204, 245, 38, 39, 199, 158, 125, 48, 148, + 100, 155, 174, 225, 207, 30, 60, 217, 107, 53, 45, 76, 58, 239, 31, 75, 56, 18, + 67, 52, 207, 138, 147, 112, 109, 58, 239, 63, 22, 110, 148, 194, 142, 221, 64, + 184, 151, 80, 118, 187, 161, 254, 80, 50, 53, 236, 2, + ], + is_vote: false, + transaction: Some(Transaction { + signatures: vec![vec![ + 22, 112, 23, 161, 121, 241, 200, 28, 204, 245, 38, 39, 199, 158, 125, 48, + 148, 100, 155, 174, 225, 207, 30, 60, 217, 107, 53, 45, 76, 58, 239, 31, + 75, 56, 18, 67, 52, 207, 138, 147, 112, 109, 58, 239, 63, 22, 110, 148, + 194, 142, 221, 64, 184, 151, 80, 118, 187, 161, 254, 80, 50, 53, 236, 2, + ]], + message: Some(Message { + header: Some(MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 5, + }), + account_keys, + recent_blockhash: vec![ + 209, 223, 101, 156, 149, 73, 16, 29, 49, 253, 251, 190, 197, 204, 84, + 130, 130, 147, 250, 9, 28, 172, 9, 188, 110, 174, 188, 249, 70, 223, + 30, 240, + ], + instructions, + versioned: true, + address_table_lookups: vec![], + }), + }), + meta: Some(TransactionStatusMeta { + err: None, + fee: 21800, + pre_balances: vec![ + 11087665779, + 492886560, + 0, + 117230240, + 1988489818, + 1002240, + 1586880, + 0, + 1559040, + 0, + 1, + 1, + 1141440, + 1141440, + 0, + ], + post_balances: vec![ + 11087643979, + 493156560, + 0, + 120076160, + 1988519818, + 1002240, + 0, + 0, + 0, + 0, + 1, + 1, + 1141440, + 1141440, + 0, + ], + inner_instructions, + inner_instructions_none: false, + log_messages: vec![ + "Program ComputeBudget111111111111111111111111111111 invoke [1]".into(), + "Program ComputeBudget111111111111111111111111111111 success".into(), + "Program DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh invoke [1]".into(), + "Program log: Instruction: ProcessUndelegation".into(), + "Program DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh success".into(), + ], + log_messages_none: false, + pre_token_balances: vec![], + post_token_balances: vec![], + rewards: vec![], + loaded_writable_addresses: vec![], + loaded_readonly_addresses: vec![], + return_data: None, + return_data_none: true, + compute_units_consumed: Some(129839), + }), + index: 1, + }), + }; + + let result = process_update(&txn).collect::>(); + + assert_eq!(result.len(), 1, "Should detect exactly one undelegation"); + let expected_record: Pubkey = [ + 191, 216, 249, 0, 219, 24, 217, 241, 219, 105, 68, 147, 70, 243, 61, 204, 216, 198, + 228, 218, 222, 249, 42, 129, 249, 80, 136, 232, 65, 137, 1, 40, + ]; + assert_eq!( + result[0], expected_record, + "Record pubkey should match account at index 6" + ); + } } From bb861ef1d031a974d12eeda9681fa67e7f882307 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 21 Jan 2026 17:40:41 -0700 Subject: [PATCH 16/16] chore: adjust delegation record account index to make _real_ test pass --- src/consts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/consts.rs b/src/consts.rs index 1e75036..b521201 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -22,7 +22,7 @@ pub(crate) const UNDELEGATE_DISCRIMINATOR: u8 = 3; pub(crate) const DISCRIMINATOR_LEN: usize = 8; /// Index of the delegation record account in undelegate instruction accounts. -pub(crate) const DELEGATION_RECORD_ACCOUNT_INDEX: usize = 6; +pub(crate) const DELEGATION_RECORD_ACCOUNT_INDEX: usize = 7; /// Maximum pending subscription/unsubscription requests. pub(crate) const MAX_PENDING_REQUESTS: usize = 256; @@ -31,4 +31,4 @@ pub(crate) const MAX_PENDING_REQUESTS: usize = 256; pub(crate) const MAX_PENDING_UPDATES: usize = 8192; /// Maximum reconnection attempts to the Laserstream. -pub(crate) const MAX_RECONNECT_ATTEMPTS: u32 = 16; +pub(crate) const MAX_RECONNECT_ATTEMPTS: u32 = 16; \ No newline at end of file