2 unstable releases

Uses new Rust 2024

new 0.2.0-rc2+solana.2 Jul 10, 2025
0.2.0-rc1+solana.2 Jul 9, 2025
0.2.0-pre.2+solana.2.1 May 16, 2025
0.1.1-pre.2+solana.2.1.11 Feb 20, 2025

#671 in Magic Beans

Download history 1/week @ 2025-04-04 9/week @ 2025-04-11 87/week @ 2025-05-02 40/week @ 2025-05-09 143/week @ 2025-05-16 260/week @ 2025-07-04

265 downloads per month
Used in yellowstone-fumarole-cli

AGPL-3.0

100KB
2K SLoC

Yellowstone Fumarole Client

See repository for more details


lib.rs:

A Rust implementation of the Yellowstone Fumarole Client using Tokio and Tonic.

Fumarole Client uses gRPC connections to communicate with the Fumarole service.

Yellowstone-GRPC vs Yellowstone-Fumarole

For the most part, the API is similar to the original yellowstone-grpc client.

However, there are some differences:

  • The yellowstone-fumarole (Coming soon) client uses multiple gRPC connections to communicate with the Fumarole service : avoids HoL blocking.
  • The yellowstone-fumarole subscribers are persistent and can be reused across multiple sessions (not computer).
  • The yellowstone-fumarole can reconnect to the Fumarole service if the connection is lost.

Examples

Examples can be found in the examples directory.

Create a FumaroleClient

To create a FumaroleClient, you need to provide a configuration object.

use yellowstone_fumarole_client::FumaroleClient;
use yellowstone_fumarole_client::config::FumaroleConfig;

#[tokio::main]
async fn main() {
    let config = FumaroleConfig {
        endpoint: "https://example.com".to_string(),
        x_token: Some("00000000-0000-0000-0000-000000000000".to_string()),
        max_decoding_message_size_bytes: FumaroleConfig::default_max_decoding_message_size_bytes(),
        x_metadata: Default::default(),
    };
    let fumarole_client = FumaroleClient::connect(config)
        .await
        .expect("Failed to connect to fumarole");
}

The prefered way to create FumaroleConfig is use serde_yaml to deserialize from a YAML file.

let config_file = std::fs::File::open("path/to/config.yaml").unwrap();
let config: FumaroleConfig = serde_yaml::from_reader(config_file).unwrap();

Here's an example of a YAML file:

endpoint: https://example.com
x-token: 00000000-0000-0000-0000-000000000000

Dragonsmouth-like Subscribe

use {
    clap::Parser,
    solana_sdk::{bs58, pubkey::Pubkey},
    std::{collections::HashMap, path::PathBuf},
    yellowstone_fumarole_client::{
        config::FumaroleConfig, DragonsmouthAdapterSession, FumaroleClient,
    },
    yellowstone_grpc_proto::geyser::{
        subscribe_update::UpdateOneof, SubscribeRequest,
        SubscribeRequestFilterTransactions, SubscribeUpdateAccount, SubscribeUpdateTransaction,
    },
};

#[derive(Debug, Clone, Parser)]
#[clap(author, version, about = "Yellowstone Fumarole Example")]
struct Args {
    /// Path to static config file
    #[clap(long)]
    config: PathBuf,

    #[clap(subcommand)]
    action: Action,
}

#[derive(Debug, Clone, Parser)]
enum Action {
    /// Subscribe to fumarole events
    Subscribe(SubscribeArgs),
}

#[derive(Debug, Clone, Parser)]
struct SubscribeArgs {
    /// Name of the persistent subscriber to use
    #[clap(long)]
    name: String,
}

fn summarize_account(account: SubscribeUpdateAccount) -> Option<String> {
    let slot = account.slot;
    let account = account.account?;
    let pubkey = Pubkey::try_from(account.pubkey).expect("Failed to parse pubkey");
    let owner = Pubkey::try_from(account.owner).expect("Failed to parse owner");
    Some(format!("account,{},{},{}", slot, pubkey, owner))
}

fn summarize_tx(tx: SubscribeUpdateTransaction) -> Option<String> {
    let slot = tx.slot;
    let tx = tx.transaction?;
    let sig = bs58::encode(tx.signature).into_string();
    Some(format!("tx,{slot},{sig}"))
}

async fn subscribe(args: SubscribeArgs, config: FumaroleConfig) {
    // This request listen for all account updates and transaction updates
    let request = SubscribeRequest {
        transactions: HashMap::from([(
            "f1".to_owned(),
            SubscribeRequestFilterTransactions::default(),
        )]),
        ..Default::default()
    };

    let mut fumarole_client = FumaroleClient::connect(config)
        .await
        .expect("Failed to connect to fumarole");

    let dragonsmouth_session = fumarole_client
        .dragonsmouth_subscribe(args.name, request)
        .await
        .expect("Failed to subscribe");

    let DragonsmouthAdapterSession {
        sink: _,
        mut source,
        mut fumarole_handle,
    } = dragonsmouth_session;
    
    loop {

        tokio::select! {
            result = &mut fumarole_handle => {
                eprintln!("Fumarole handle closed: {:?}", result);
                break;
            }
            maybe = source.recv() => {
                match maybe {
                    None => {
                        eprintln!("Source closed");
                        break;
                    }
                    Some(result) => {
                        let event = result.expect("Failed to receive event");
                        let message = if let Some(oneof) = event.update_oneof {
                            match oneof {
                                UpdateOneof::Account(account_update) => {
                                    summarize_account(account_update)
                                }
                                UpdateOneof::Transaction(tx) => {
                                    summarize_tx(tx)
                                }                    
                                _ => None,
                            }
                        } else {
                            None
                        };

                        if let Some(message) = message {
                            println!("{}", message);
                        }
                    }
                }
            }
        }
    }
}

#[tokio::main]
async fn main() {
    let args: Args = Args::parse();
    let config = std::fs::read_to_string(&args.config).expect("Failed to read config file");
    let config: FumaroleConfig =
        serde_yaml::from_str(&config).expect("Failed to parse config file");

    match args.action {
        Action::Subscribe(sub_args) => {
            subscribe(sub_args, config).await;
        }
    }
}

Enable Prometheus Metrics

To enable Prometheus metrics, add the features = [prometheus] to your Cargo.toml file:

[dependencies]
yellowstone-fumarole-client = { version = "x.y.z", features = ["prometheus"] }

Then, you can use the metrics module to register and expose metrics:

use yellowstone_fumarole_client::metrics;
use prometheus::{Registry};

let r = Registry::new();

metrics::register_metrics(&r);

// After registering, you should see `fumarole_` prefixed metrics in the registry.

Getting Started

Follows the instruction in the README file to get started.

Feature Flags

  • prometheus: Enables Prometheus metrics for the Fumarole client.

Dependencies

~17–31MB
~577K SLoC