Skip to main content

surfpool_core/surfnet/
mod.rs

1use std::{collections::HashMap, fmt::Display};
2
3use crossbeam_channel::Sender;
4use jsonrpc_core::Result as RpcError;
5use locker::SurfnetSvmLocker;
6use solana_account::Account;
7use solana_account_decoder::{UiAccount, UiAccountEncoding};
8use solana_client::{
9    rpc_config::RpcTransactionLogsFilter,
10    rpc_filter::RpcFilterType,
11    rpc_response::{RpcKeyedAccount, RpcLogsResponse},
12};
13use solana_clock::Slot;
14use solana_commitment_config::CommitmentLevel;
15use solana_epoch_info::EpochInfo;
16use solana_pubkey::Pubkey;
17use solana_signature::Signature;
18use solana_transaction::versioned::VersionedTransaction;
19use solana_transaction_error::TransactionError;
20use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, TransactionStatus};
21use svm::SurfnetSvm;
22
23use crate::{
24    error::{SurfpoolError, SurfpoolResult},
25    types::{GeyserAccountUpdate, TransactionWithStatusMeta},
26};
27
28pub mod locker;
29pub mod remote;
30pub mod surfnet_lite_svm;
31pub mod svm;
32
33pub const FINALIZATION_SLOT_THRESHOLD: u64 = 31;
34pub const SLOTS_PER_EPOCH: u64 = 432000;
35
36pub type AccountFactory = Box<dyn Fn(SurfnetSvmLocker) -> GetAccountResult + Send + Sync>;
37
38/// Slot status for geyser plugin notifications.
39/// Mirrors `agave_geyser_plugin_interface::geyser_plugin_interface::SlotStatus`.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum GeyserSlotStatus {
42    /// Slot is being processed
43    Processed,
44    /// Slot has been rooted (finalized)
45    Rooted,
46    /// Slot has been confirmed
47    Confirmed,
48}
49
50/// Block metadata for geyser plugin notifications.
51#[derive(Debug, Clone)]
52pub struct GeyserBlockMetadata {
53    pub slot: Slot,
54    pub blockhash: String,
55    pub parent_slot: Slot,
56    pub parent_blockhash: String,
57    pub block_time: Option<i64>,
58    pub block_height: Option<u64>,
59    pub executed_transaction_count: u64,
60    pub entry_count: u64,
61}
62
63/// Entry info for geyser plugin notifications.
64/// Surfpool emits one entry per block (simplified model).
65#[derive(Debug, Clone)]
66pub struct GeyserEntryInfo {
67    pub slot: Slot,
68    pub index: usize,
69    pub num_hashes: u64,
70    pub hash: Vec<u8>,
71    pub executed_transaction_count: u64,
72    pub starting_transaction_index: usize,
73}
74
75#[allow(clippy::large_enum_variant)]
76pub enum GeyserEvent {
77    NotifyTransaction(TransactionWithStatusMeta, Option<VersionedTransaction>),
78    UpdateAccount(GeyserAccountUpdate),
79    /// Account update sent at startup (before block production begins).
80    /// These updates should be sent to geyser plugins with is_startup=true.
81    StartupAccountUpdate(GeyserAccountUpdate),
82    /// Notify plugins that startup is complete.
83    EndOfStartup,
84    /// Update slot status (processed, confirmed, rooted/finalized).
85    UpdateSlotStatus {
86        slot: Slot,
87        parent: Option<Slot>,
88        status: GeyserSlotStatus,
89    },
90    /// Notify plugins of block metadata.
91    NotifyBlockMetadata(GeyserBlockMetadata),
92    /// Notify plugins of entry execution.
93    NotifyEntry(GeyserEntryInfo),
94}
95
96#[derive(Debug, Eq, PartialEq, Hash, Clone)]
97pub struct BlockIdentifier {
98    pub index: u64,
99    pub hash: String,
100}
101
102impl BlockIdentifier {
103    pub fn zero() -> Self {
104        Self::new(
105            0,
106            "0000000000000000000000000000000000000000000000000000000000000000",
107        )
108    }
109
110    pub fn new(index: u64, hash: &str) -> Self {
111        Self {
112            index,
113            hash: hash.to_string(),
114        }
115    }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct BlockHeader {
120    pub hash: String,
121    pub previous_blockhash: String,
122    pub parent_slot: Slot,
123    pub block_time: i64,
124    pub block_height: u64,
125    pub signatures: Vec<Signature>,
126}
127
128#[derive(PartialEq, Eq, Clone)]
129pub enum SurfnetDataConnection {
130    Offline,
131    Connected(String, EpochInfo),
132}
133
134pub type SignatureSubscriptionData = (
135    SignatureSubscriptionType,
136    Sender<(Slot, Option<TransactionError>)>,
137);
138
139pub type AccountSubscriptionData =
140    HashMap<Pubkey, Vec<(Option<UiAccountEncoding>, Sender<UiAccount>)>>;
141
142pub type ProgramSubscriptionData = HashMap<
143    Pubkey,
144    Vec<(
145        Option<UiAccountEncoding>,
146        Option<Vec<RpcFilterType>>,
147        Sender<RpcKeyedAccount>,
148    )>,
149>;
150
151pub type LogsSubscriptionData = (
152    CommitmentLevel,
153    RpcTransactionLogsFilter,
154    Sender<(Slot, RpcLogsResponse)>,
155);
156
157pub type SnapshotSubscriptionData = Sender<SnapshotImportNotification>;
158
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160pub struct SnapshotImportNotification {
161    pub snapshot_id: String,
162    pub status: SnapshotImportStatus,
163    pub accounts_loaded: u64,
164    pub total_accounts: u64,
165    pub error: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
169pub enum SnapshotImportStatus {
170    Started,
171    InProgress,
172    Completed,
173    Failed,
174}
175
176#[derive(Debug, Clone, PartialEq)]
177pub enum SignatureSubscriptionType {
178    Received,
179    Commitment(CommitmentLevel),
180}
181
182impl Display for SignatureSubscriptionType {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        match self {
185            SignatureSubscriptionType::Received => write!(f, "received"),
186            SignatureSubscriptionType::Commitment(level) => write!(f, "{level}"),
187        }
188    }
189}
190
191type DoUpdateSvm = bool;
192
193#[derive(Clone, Debug)]
194/// Represents the result of a get_account operation.
195pub enum GetAccountResult {
196    /// Represents that the account was not found.
197    None(Pubkey),
198    /// Represents that the account was found.
199    /// The `DoUpdateSvm` flag indicates whether the SVM should be updated after this account is found.
200    /// This is useful for cases where the account was fetched from a remote source and needs to be
201    /// updated in the SVM to reflect the latest state. However, when the account is found locally,
202    /// it likely does not need to be updated in the SVM.
203    FoundAccount(Pubkey, Account, DoUpdateSvm),
204    FoundProgramAccount((Pubkey, Account), (Pubkey, Option<Account>)),
205    FoundTokenAccount((Pubkey, Account), (Pubkey, Option<Account>)),
206}
207
208impl GetAccountResult {
209    pub fn expected_data(&self) -> &Vec<u8> {
210        match &self {
211            Self::None(_) => unreachable!(),
212            Self::FoundAccount(_, account, _)
213            | Self::FoundProgramAccount((_, account), _)
214            | Self::FoundTokenAccount((_, account), _) => &account.data,
215        }
216    }
217
218    pub fn apply_update<T>(&mut self, update: T) -> RpcError<()>
219    where
220        T: Fn(&mut Account) -> RpcError<()>,
221    {
222        match self {
223            Self::None(_) => unreachable!(),
224            Self::FoundAccount(_, account, do_update_account) => {
225                update(account)?;
226                *do_update_account = true;
227            }
228            Self::FoundProgramAccount((_, account), _) => {
229                update(account)?;
230            }
231            Self::FoundTokenAccount((_, account), _) => {
232                update(account)?;
233            }
234        }
235        Ok(())
236    }
237
238    pub fn map_account(self) -> SurfpoolResult<Account> {
239        match self {
240            Self::None(pubkey) => Err(SurfpoolError::account_not_found(pubkey)),
241            Self::FoundAccount(_, account, _)
242            | Self::FoundProgramAccount((_, account), _)
243            | Self::FoundTokenAccount((_, account), _) => Ok(account),
244        }
245    }
246
247    #[allow(clippy::type_complexity)]
248    pub fn map_account_with_token_data(
249        self,
250    ) -> Option<((Pubkey, Account), Option<(Pubkey, Option<Account>)>)> {
251        match self {
252            Self::None(_) => None,
253            Self::FoundAccount(pubkey, account, _) => Some(((pubkey, account), None)),
254            Self::FoundProgramAccount((pubkey, account), _) => Some(((pubkey, account), None)),
255            Self::FoundTokenAccount((pubkey, account), token_data) => {
256                Some(((pubkey, account), Some(token_data)))
257            }
258        }
259    }
260
261    pub const fn is_none(&self) -> bool {
262        matches!(self, Self::None(_))
263    }
264
265    pub const fn requires_update(&self) -> bool {
266        match self {
267            Self::None(_) => false,
268            Self::FoundAccount(_, _, do_update) => *do_update,
269            Self::FoundProgramAccount(_, _) => true,
270            Self::FoundTokenAccount(_, _) => true,
271        }
272    }
273}
274
275impl From<GetAccountResult> for Result<Account, SurfpoolError> {
276    fn from(value: GetAccountResult) -> Self {
277        value.map_account()
278    }
279}
280
281impl SignatureSubscriptionType {
282    pub const fn received() -> Self {
283        SignatureSubscriptionType::Received
284    }
285
286    pub const fn processed() -> Self {
287        SignatureSubscriptionType::Commitment(CommitmentLevel::Processed)
288    }
289
290    pub const fn confirmed() -> Self {
291        SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed)
292    }
293
294    pub const fn finalized() -> Self {
295        SignatureSubscriptionType::Commitment(CommitmentLevel::Finalized)
296    }
297}
298
299#[allow(clippy::large_enum_variant)]
300pub enum GetTransactionResult {
301    None(Signature),
302    FoundTransaction(
303        Signature,
304        EncodedConfirmedTransactionWithStatusMeta,
305        TransactionStatus,
306    ),
307}
308
309impl GetTransactionResult {
310    pub fn found_transaction(
311        signature: Signature,
312        tx: EncodedConfirmedTransactionWithStatusMeta,
313        latest_absolute_slot: u64,
314    ) -> Self {
315        let is_finalized = latest_absolute_slot >= tx.slot + FINALIZATION_SLOT_THRESHOLD;
316        let is_confirmed = latest_absolute_slot >= tx.slot + 1;
317        let (confirmation_status, confirmations) = if is_finalized {
318            (
319                Some(solana_transaction_status::TransactionConfirmationStatus::Finalized),
320                None,
321            )
322        } else if is_confirmed {
323            (
324                Some(solana_transaction_status::TransactionConfirmationStatus::Confirmed),
325                Some((latest_absolute_slot - tx.slot) as usize),
326            )
327        } else {
328            (
329                Some(solana_transaction_status::TransactionConfirmationStatus::Processed),
330                Some((latest_absolute_slot - tx.slot) as usize),
331            )
332        };
333        let status = TransactionStatus {
334            slot: tx.slot,
335            confirmations,
336            status: tx
337                .transaction
338                .clone()
339                .meta
340                .map_or(Ok(()), |m| m.status.map_err(|e| e.into())),
341            err: tx
342                .transaction
343                .clone()
344                .meta
345                .and_then(|m| m.err.map(|e| e.into())),
346            confirmation_status,
347        };
348
349        Self::FoundTransaction(signature, tx, status)
350    }
351
352    pub const fn is_none(&self) -> bool {
353        matches!(self, Self::None(_))
354    }
355
356    pub fn map_found_transaction(&self) -> SurfpoolResult<TransactionStatus> {
357        match self {
358            Self::None(sig) => Err(SurfpoolError::transaction_not_found(sig)),
359            Self::FoundTransaction(_, _, status) => Ok(status.clone()),
360        }
361    }
362
363    pub fn map_some_transaction_status(&self) -> Option<TransactionStatus> {
364        match self {
365            Self::None(_) => None,
366            Self::FoundTransaction(_, _, status) => Some(status.clone()),
367        }
368    }
369}