yellowstone_vixen_core/
lib.rs

1#![deny(
2    clippy::disallowed_methods,
3    clippy::suspicious,
4    clippy::style,
5    clippy::clone_on_ref_ptr,
6    missing_debug_implementations,
7    missing_copy_implementations
8)]
9#![warn(clippy::pedantic, missing_docs)]
10#![allow(clippy::module_name_repetitions)]
11
12//! This crate provides the core components necessary for implementing parsers
13//! for the `yellowstone-vixen` family of crates.  This crate should be used
14//! as a dependency instead of `yellowstone-vixen` for crates that intend to
15//! define and export Vixen parsers as libraries without needing to access the
16//! runtime functionality of Vixen.
17
18use std::{
19    borrow::Cow,
20    collections::{HashMap, HashSet},
21    fmt::{self, Debug},
22    future::Future,
23    ops,
24    str::FromStr,
25};
26
27use yellowstone_grpc_proto::geyser::{
28    SubscribeRequest, SubscribeRequestFilterAccounts, SubscribeRequestFilterTransactions,
29    SubscribeUpdateAccount, SubscribeUpdateTransaction,
30};
31
32pub extern crate bs58;
33#[cfg(feature = "proto")]
34pub extern crate yellowstone_vixen_proto;
35
36pub mod instruction;
37#[cfg(feature = "proto")]
38pub mod proto;
39
40type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
41
42/// An error returned by a Vixen parser
43#[derive(Debug)]
44pub enum ParseError {
45    /// The parser received an undesired update and requested to skip
46    /// processing for it.  No error will be logged by the Vixen runtime, and
47    /// no handlers registered to this parser will be executed.
48    Filtered,
49    /// The parser encountered an error while processing an update.
50    Other(BoxedError),
51}
52
53impl<T: Into<BoxedError>> From<T> for ParseError {
54    #[inline]
55    fn from(value: T) -> Self { Self::Other(value.into()) }
56}
57
58/// The result of parsing an update.
59pub type ParseResult<T> = Result<T, ParseError>;
60
61/// An account update from Yellowstone.
62pub type AccountUpdate = SubscribeUpdateAccount;
63/// A transaction update from Yellowstone.
64pub type TransactionUpdate = SubscribeUpdateTransaction;
65
66/// A core trait that defines the parse logic for producing a parsed value from
67/// a Vixen update (typically [`AccountUpdate`], [`TransactionUpdate`], or
68/// [`InstructionUpdate`](instruction::InstructionUpdate)).
69pub trait Parser {
70    /// The input update type for this parser.
71    type Input;
72    /// The type of the parsed value produced by this parser.
73    type Output;
74
75    /// A unique ID for this parser.  Used to associate the parser with its
76    /// requested prefilter data.
77    ///
78    /// **NOTE:** For parsers that do not accept configuration when constructed
79    /// (e.g. a parser that accepts all updates of a certain type from a
80    /// specific program), the ID may be as simple as the fully-qualified type
81    /// name of the parser.  However, for parsers that produce a different
82    /// prefilter depending on some internal configuration, instances that
83    /// output differing prefilters _must_ output different IDs.
84    fn id(&self) -> Cow<str>;
85
86    /// Filter data passed to Yellowstone to coarsely narrow down updates
87    /// to values parseable by this parser.
88    fn prefilter(&self) -> Prefilter;
89
90    /// Parse the given update into a parsed value.
91    fn parse(&self, value: &Self::Input) -> impl Future<Output = ParseResult<Self::Output>> + Send;
92}
93
94/// A parser that parses all relevant updates for a particular program ID.
95pub trait ProgramParser: Parser {
96    /// The program ID that this parser is associated with.
97    fn program_id(&self) -> Pubkey;
98}
99
100/// Helper trait for getting the ID of a parser.
101pub trait ParserId {
102    /// Get the ID of this parser, see [`Parser::id`].
103    fn id(&self) -> Cow<str>;
104}
105
106impl ParserId for std::convert::Infallible {
107    #[inline]
108    fn id(&self) -> Cow<str> { match *self {} }
109}
110
111impl<T: Parser> ParserId for T {
112    #[inline]
113    fn id(&self) -> Cow<str> { Parser::id(self) }
114}
115
116/// Helper trait for getting the prefilter of a parser.
117pub trait GetPrefilter {
118    /// Get the prefilter of this parser, see [`Parser::prefilter`].
119    fn prefilter(&self) -> Prefilter;
120}
121
122impl GetPrefilter for std::convert::Infallible {
123    #[inline]
124    fn prefilter(&self) -> Prefilter { match *self {} }
125}
126
127impl<T: Parser> GetPrefilter for T {
128    #[inline]
129    fn prefilter(&self) -> Prefilter { Parser::prefilter(self) }
130}
131
132// TODO: why are so many fields on the prefilters and prefilter builder optional???
133/// A prefilter for narrowing down the updates that a parser will receive.
134#[derive(Debug, Default)]
135pub struct Prefilter {
136    pub(crate) account: Option<AccountPrefilter>,
137    pub(crate) transaction: Option<TransactionPrefilter>,
138}
139
140fn merge_opt<T, F: FnOnce(&mut T, T)>(lhs: &mut Option<T>, rhs: Option<T>, f: F) {
141    match (lhs.as_mut(), rhs) {
142        (None, r) => *lhs = r,
143        (Some(_), None) => (),
144        (Some(l), Some(r)) => f(l, r),
145    }
146}
147
148impl Prefilter {
149    /// Create a new prefilter builder.
150    #[inline]
151    pub fn builder() -> PrefilterBuilder { PrefilterBuilder::default() }
152
153    /// Merge another prefilter into this one, producing a prefilter that
154    /// describes the union of the two.
155    pub fn merge(&mut self, other: Prefilter) {
156        let Self {
157            account,
158            transaction,
159        } = self;
160        merge_opt(account, other.account, AccountPrefilter::merge);
161        merge_opt(transaction, other.transaction, TransactionPrefilter::merge);
162    }
163}
164
165impl FromIterator<Prefilter> for Prefilter {
166    fn from_iter<T: IntoIterator<Item = Prefilter>>(iter: T) -> Self {
167        let mut iter = iter.into_iter();
168        let Some(ret) = iter.next() else {
169            return Self::default();
170        };
171        iter.fold(ret, |mut l, r| {
172            l.merge(r);
173            l
174        })
175    }
176}
177
178#[derive(Debug, Default, Clone, PartialEq)]
179pub(crate) struct AccountPrefilter {
180    pub accounts: HashSet<Pubkey>,
181    pub owners: HashSet<Pubkey>,
182}
183
184impl AccountPrefilter {
185    pub fn merge(&mut self, other: AccountPrefilter) {
186        let Self { accounts, owners } = self;
187        accounts.extend(other.accounts);
188        owners.extend(other.owners);
189    }
190}
191
192#[derive(Debug, Default, Clone, PartialEq)]
193pub(crate) struct TransactionPrefilter {
194    pub accounts: HashSet<Pubkey>,
195}
196
197impl TransactionPrefilter {
198    pub fn merge(&mut self, other: TransactionPrefilter) {
199        let Self { accounts } = self;
200        accounts.extend(other.accounts);
201    }
202}
203
204/// Helper macro for converting Vixen's [`Pubkey`] to a Solana ed25519 public
205/// key.
206///
207/// Invoking the macro with the name of a publicly-exported Solana `Pubkey`
208/// type (e.g. `pubkey_convert_helpers!(solana_sdk::pubkey::Pubkey);`) will
209/// define two functions:
210///
211/// - `pub(crate) fn into_vixen_pubkey(`<Solana Pubkey>`) -> yellowstone_vixen_core::Pubkey;`
212/// - `pub(crate) fn from_vixen_pubkey(yellowstone_vixen_core::Pubkey) -> <Solana Pubkey>;`
213///
214/// These can be used as a convenience for quickly converting between Solana
215/// public keys and their representation in Vixen.  Vixen does not use the
216/// built-in Solana `Pubkey` type, nor does it provide `From`/`Into` impls for
217/// it, to avoid creating an unnecessary dependency on any specific version of
218/// the full Solana SDK.
219#[macro_export]
220macro_rules! pubkey_convert_helpers {
221    ($ty:ty) => {
222        pub(crate) fn into_vixen_pubkey(value: $ty) -> $crate::Pubkey { value.to_bytes().into() }
223
224        pub(crate) fn from_vixen_pubkey(value: $crate::Pubkey) -> $ty { value.into_bytes().into() }
225    };
226}
227
228/// Helper type representing a Solana public key.
229///
230/// This type is functionally equivalent to the `Pubkey` type from the Solana
231/// SDK, and it can be trivially converted to or from one by passing the
232/// underlying `[u8; 32]` array.  Vixen uses this `Pubkey` type to avoid
233/// depending on the Solana SDK, as this can lead to version conflicts when
234/// working with Solana program crates.
235pub type Pubkey = KeyBytes<32>;
236
237/// Generic wrapper for a fixed-length array of cryptographic key bytes,
238/// convertible to or from a base58-encoded string.
239#[derive(Clone, Copy, PartialEq, Eq, Hash)]
240#[repr(transparent)]
241pub struct KeyBytes<const LEN: usize>(pub [u8; LEN]);
242
243impl<const LEN: usize> Debug for KeyBytes<LEN> {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        f.debug_tuple("KeyBytes")
246            .field(&bs58::encode(self.0).into_string())
247            .finish()
248    }
249}
250
251impl<const LEN: usize> fmt::Display for KeyBytes<LEN> {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        f.write_str(&bs58::encode(self.0).into_string())
254    }
255}
256
257impl<const LEN: usize> From<[u8; LEN]> for KeyBytes<LEN> {
258    #[inline]
259    fn from(value: [u8; LEN]) -> Self { Self(value) }
260}
261
262impl<const LEN: usize> From<KeyBytes<LEN>> for [u8; LEN] {
263    #[inline]
264    fn from(value: KeyBytes<LEN>) -> Self { value.0 }
265}
266
267impl<const LEN: usize> std::ops::Deref for KeyBytes<LEN> {
268    type Target = [u8; LEN];
269
270    fn deref(&self) -> &Self::Target { &self.0 }
271}
272
273impl<const LEN: usize> std::ops::DerefMut for KeyBytes<LEN> {
274    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
275}
276
277impl<const LEN: usize> AsRef<[u8; LEN]> for KeyBytes<LEN> {
278    fn as_ref(&self) -> &[u8; LEN] { self }
279}
280
281impl<const LEN: usize> AsMut<[u8; LEN]> for KeyBytes<LEN> {
282    fn as_mut(&mut self) -> &mut [u8; LEN] { self }
283}
284
285impl<const LEN: usize> std::borrow::Borrow<[u8; LEN]> for KeyBytes<LEN> {
286    fn borrow(&self) -> &[u8; LEN] { self }
287}
288
289impl<const LEN: usize> std::borrow::BorrowMut<[u8; LEN]> for KeyBytes<LEN> {
290    fn borrow_mut(&mut self) -> &mut [u8; LEN] { self }
291}
292
293impl<const LEN: usize> AsRef<[u8]> for KeyBytes<LEN> {
294    fn as_ref(&self) -> &[u8] { self.as_slice() }
295}
296
297impl<const LEN: usize> AsMut<[u8]> for KeyBytes<LEN> {
298    fn as_mut(&mut self) -> &mut [u8] { self.as_mut_slice() }
299}
300
301impl<const LEN: usize> std::borrow::Borrow<[u8]> for KeyBytes<LEN> {
302    fn borrow(&self) -> &[u8] { self.as_ref() }
303}
304
305impl<const LEN: usize> std::borrow::BorrowMut<[u8]> for KeyBytes<LEN> {
306    fn borrow_mut(&mut self) -> &mut [u8] { self.as_mut() }
307}
308
309type KeyFromSliceError = std::array::TryFromSliceError;
310
311impl<const LEN: usize> TryFrom<&[u8]> for KeyBytes<LEN> {
312    type Error = KeyFromSliceError;
313
314    #[inline]
315    fn try_from(value: &[u8]) -> Result<Self, Self::Error> { value.try_into().map(Self) }
316}
317
318impl<const LEN: usize> KeyBytes<LEN> {
319    /// Construct a new instance from the provided key bytes
320    #[must_use]
321    pub fn new(bytes: [u8; LEN]) -> Self { bytes.into() }
322
323    /// Return the public key bytes contained in this instance
324    #[must_use]
325    pub fn into_bytes(self) -> [u8; LEN] { self.into() }
326
327    /// Attempt to convert the provided byte slice to a new key byte array
328    ///
329    /// # Errors
330    /// This function returns an error if calling `KeyBytes::try_from(slice)`
331    /// returns an error.
332    pub fn try_from_ref<T: AsRef<[u8]>>(key: T) -> Result<Self, KeyFromSliceError> {
333        key.as_ref().try_into()
334    }
335
336    /// Compare the public key bytes contained in this array with the given byte
337    /// slice
338    pub fn equals_ref<T: AsRef<[u8]>>(&self, other: T) -> bool {
339        self.as_slice().eq(other.as_ref())
340    }
341}
342
343/// An error that can occur when parsing a key from a base58 string.
344#[derive(Debug, Clone, Copy, thiserror::Error)]
345pub enum KeyFromStrError<const LEN: usize = 32> {
346    /// The string was not a valid base58 string.
347    #[error("Invalid base58 string")]
348    Bs58(#[from] bs58::decode::Error),
349    /// The parsed base58 data was not the correct length for a public key.
350    #[error("Invalid key length, must be {LEN} bytes")]
351    Len(#[from] std::array::TryFromSliceError),
352}
353
354impl<const LEN: usize> FromStr for KeyBytes<LEN> {
355    type Err = KeyFromStrError<LEN>;
356
357    fn from_str(s: &str) -> Result<Self, Self::Err> {
358        bs58::decode(s)
359            .into_vec()?
360            .as_slice()
361            .try_into()
362            .map_err(Into::into)
363    }
364}
365
366impl<const LEN: usize> TryFrom<&str> for KeyBytes<LEN> {
367    type Error = KeyFromStrError<LEN>;
368
369    fn try_from(value: &str) -> Result<Self, Self::Error> { value.parse() }
370}
371
372impl<const LEN: usize> TryFrom<String> for KeyBytes<LEN> {
373    type Error = KeyFromStrError<LEN>;
374
375    fn try_from(value: String) -> Result<Self, Self::Error> { value.parse() }
376}
377
378impl<const LEN: usize> TryFrom<Cow<'_, str>> for KeyBytes<LEN> {
379    type Error = KeyFromStrError<LEN>;
380
381    fn try_from(value: Cow<str>) -> Result<Self, Self::Error> { value.parse() }
382}
383
384/// An error that can occur when building a prefilter.
385#[derive(Debug, Clone, thiserror::Error)]
386pub enum PrefilterError {
387    /// A value was already set for a field that can only be set once.
388    #[error("Value already given for field {0}")]
389    AlreadySet(&'static str),
390    /// An error occurred while parsing a public key as a [`Pubkey`].
391    #[error("Invalid pubkey {}", bs58::encode(.0).into_string())]
392    BadPubkey(Vec<u8>, std::array::TryFromSliceError),
393}
394
395/// A builder for constructing a prefilter.
396#[derive(Debug, Default)]
397#[must_use = "Consider calling .build() on this builder"]
398pub struct PrefilterBuilder {
399    error: Option<PrefilterError>,
400    accounts: Option<HashSet<Pubkey>>,
401    account_owners: Option<HashSet<Pubkey>>,
402    transaction_accounts: Option<HashSet<Pubkey>>,
403}
404
405fn set_opt<T>(opt: &mut Option<T>, field: &'static str, val: T) -> Result<(), PrefilterError> {
406    if opt.is_some() {
407        return Err(PrefilterError::AlreadySet(field));
408    }
409
410    *opt = Some(val);
411    Ok(())
412}
413
414// TODO: if Solana ever adds Into<[u8; 32]> for Pubkey this can be simplified
415fn collect_pubkeys<I: IntoIterator>(it: I) -> Result<HashSet<Pubkey>, PrefilterError>
416where I::Item: AsRef<[u8]> {
417    it.into_iter()
418        .map(|p| {
419            let p = p.as_ref();
420            p.try_into()
421                .map_err(|e| PrefilterError::BadPubkey(p.to_vec(), e))
422        })
423        .collect()
424}
425
426impl PrefilterBuilder {
427    /// Build the prefilter from the given data.
428    ///
429    /// # Errors
430    /// Returns an error if any of the fields provided are invalid.
431    pub fn build(self) -> Result<Prefilter, PrefilterError> {
432        let PrefilterBuilder {
433            error,
434            accounts,
435            account_owners,
436            transaction_accounts,
437        } = self;
438        if let Some(err) = error {
439            return Err(err);
440        }
441
442        let account = AccountPrefilter {
443            accounts: accounts.unwrap_or_default(),
444            owners: account_owners.unwrap_or_default(),
445        };
446
447        let transaction = TransactionPrefilter {
448            accounts: transaction_accounts.unwrap_or_default(),
449        };
450
451        Ok(Prefilter {
452            account: (account != AccountPrefilter::default()).then_some(account),
453            transaction: (transaction != TransactionPrefilter::default()).then_some(transaction),
454        })
455    }
456
457    fn mutate<F: FnOnce(&mut Self) -> Result<(), PrefilterError>>(mut self, f: F) -> Self {
458        if self.error.is_none() {
459            self.error = f(&mut self).err();
460        }
461
462        self
463    }
464
465    /// Set the accounts that this prefilter will match.
466    pub fn accounts<I: IntoIterator>(self, it: I) -> Self
467    where I::Item: AsRef<[u8]> {
468        self.mutate(|this| set_opt(&mut this.accounts, "accounts", collect_pubkeys(it)?))
469    }
470
471    /// Set the `account_owners` that this prefilter will match.
472    pub fn account_owners<I: IntoIterator>(self, it: I) -> Self
473    where I::Item: AsRef<[u8]> {
474        self.mutate(|this| {
475            set_opt(
476                &mut this.account_owners,
477                "account_owners",
478                collect_pubkeys(it)?,
479            )
480        })
481    }
482
483    /// Set the accounts mentioned by transactions that this prefilter will
484    /// match.
485    pub fn transaction_accounts<I: IntoIterator>(self, it: I) -> Self
486    where I::Item: AsRef<[u8]> {
487        self.mutate(|this| {
488            set_opt(
489                &mut this.transaction_accounts,
490                "transaction_accounts",
491                collect_pubkeys(it)?,
492            )
493        })
494    }
495}
496
497/// A collection of filters for a Vixen subscription.
498#[derive(Debug)]
499#[repr(transparent)]
500pub struct Filters<'a>(HashMap<&'a str, Prefilter>);
501
502impl<'a> Filters<'a> {
503    /// Construct a new collection of filters.
504    #[inline]
505    #[must_use]
506    pub const fn new(filters: HashMap<&'a str, Prefilter>) -> Self { Self(filters) }
507}
508
509impl<'a> ops::Deref for Filters<'a> {
510    type Target = HashMap<&'a str, Prefilter>;
511
512    #[inline]
513    fn deref(&self) -> &Self::Target { &self.0 }
514}
515
516impl<'a> From<Filters<'a>> for SubscribeRequest {
517    fn from(value: Filters<'a>) -> Self {
518        SubscribeRequest {
519            accounts: value
520                .iter()
521                .filter_map(|(k, v)| {
522                    let v = v.account.as_ref()?;
523
524                    Some((k.to_owned().into(), SubscribeRequestFilterAccounts {
525                        account: v.accounts.iter().map(ToString::to_string).collect(),
526                        owner: v.owners.iter().map(ToString::to_string).collect(),
527                        // TODO: probably a good thing to look into
528                        filters: vec![],
529                    }))
530                })
531                .collect(),
532            slots: [].into_iter().collect(),
533            transactions: value
534                .iter()
535                .filter_map(|(k, v)| {
536                    let v = v.transaction.as_ref()?;
537
538                    Some((k.to_owned().into(), SubscribeRequestFilterTransactions {
539                        vote: None,
540                        // TODO: make this configurable
541                        failed: Some(false),
542                        signature: None,
543                        // TODO: figure these out
544                        account_include: v.accounts.iter().map(ToString::to_string).collect(),
545                        account_exclude: [].into_iter().collect(),
546                        account_required: [].into_iter().collect(),
547                    }))
548                })
549                .collect(),
550            transactions_status: [].into_iter().collect(),
551            blocks: [].into_iter().collect(),
552            blocks_meta: [].into_iter().collect(),
553            entry: [].into_iter().collect(),
554            commitment: None,
555            accounts_data_slice: vec![],
556            ping: None,
557        }
558    }
559}