Geyser gRPC - 代码示例
常见 Geyser gRPC 使用场景的实用 Rust 示例。请填入您的 Geyser gRPC 端点。Solana Stream SDK 支持 Geyser gRPC。
基本订阅:所有非投票、未失败的交易
这是有意设计的宽范围交易订阅。排除失败交易可以减少流量,但剩余的流仍可能很大。请添加与消费端实际所需工作相匹配的账户、签名或交易过滤器,并将处理逻辑放在接收循环之外。
rust
use anyhow::Result;
use futures::{sink::SinkExt, stream::StreamExt};
use solana_signature::Signature;
use std::collections::HashMap;
use solana_stream_sdk::{
GeyserCommitmentLevel as CommitmentLevel, GeyserGrpcClient,
GeyserSubscribeRequest as SubscribeRequest,
GeyserSubscribeRequestFilterTransactions as SubscribeRequestFilterTransactions,
GeyserUpdateOneof as UpdateOneof,
};
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = "http://your-erpc-grpc-endpoint"; // Set your ERPC gRPC URL
let mut client = GeyserGrpcClient::build_from_shared(endpoint)?
.connect()
.await?;
let (mut tx, mut stream) = client.subscribe().await?;
tx.send(SubscribeRequest {
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false), // Drop vote txs
failed: Some(false), // Drop failed txs
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
})
.await?;
while let Some(Ok(msg)) = stream.next().await {
if let Some(UpdateOneof::Transaction(tx)) = msg.update_oneof {
let sig = tx
.transaction
.and_then(|t| Signature::try_from(t.signature.as_slice()).ok())
.unwrap_or_default();
println!("Slot: {} | Signature: {}", tx.slot, sig);
}
}
Ok(())
}use anyhow::Result;
use futures::{sink::SinkExt, stream::StreamExt};
use solana_signature::Signature;
use std::collections::HashMap;
use solana_stream_sdk::{
GeyserCommitmentLevel as CommitmentLevel, GeyserGrpcClient,
GeyserSubscribeRequest as SubscribeRequest,
GeyserSubscribeRequestFilterTransactions as SubscribeRequestFilterTransactions,
GeyserUpdateOneof as UpdateOneof,
};
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = "http://your-erpc-grpc-endpoint"; // Set your ERPC gRPC URL
let mut client = GeyserGrpcClient::build_from_shared(endpoint)?
.connect()
.await?;
let (mut tx, mut stream) = client.subscribe().await?;
tx.send(SubscribeRequest {
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false), // Drop vote txs
failed: Some(false), // Drop failed txs
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
})
.await?;
while let Some(Ok(msg)) = stream.next().await {
if let Some(UpdateOneof::Transaction(tx)) = msg.update_oneof {
let sig = tx
.transaction
.and_then(|t| Signature::try_from(t.signature.as_slice()).ok())
.unwrap_or_default();
println!("Slot: {} | Signature: {}", tx.slot, sig);
}
}
Ok(())
}持久客户端:正常关闭后的有限次重连、间隙恢复与 ping/pong
rust
use anyhow::Result;
use futures::stream::StreamExt;
use std::{collections::HashMap, time::Duration};
use solana_stream_sdk::{
GeyserCommitmentLevel as CommitmentLevel, GeyserGrpcClient,
GeyserSlotStatus as SlotStatus, GeyserSubscribeRequest as SubscribeRequest,
GeyserSubscribeRequestFilterSlots as SubscribeRequestFilterSlots,
GeyserUpdateOneof as UpdateOneof,
};
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = "http://your-erpc-grpc-endpoint";
let mut subscribe_request = SubscribeRequest {
slots: HashMap::from([(
"slots".to_string(),
SubscribeRequestFilterSlots::default(),
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
};
let mut tracked_slot: u64 = 0;
let mut retry_delay = Duration::from_secs(1);
let max_retry_delay = Duration::from_secs(30);
let mut first_diagnostic: Option<String> = None;
loop {
let mut client = match GeyserGrpcClient::build_from_shared(endpoint)?.connect().await {
Ok(client) => client,
Err(error) => {
let current_diagnostic = error.to_string();
first_diagnostic.get_or_insert_with(|| current_diagnostic.clone());
eprintln!("Connection error: {current_diagnostic}");
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
continue;
}
};
subscribe_request.from_slot = if tracked_slot > 0 {
Some(tracked_slot)
} else {
None
};
let (mut _sink, mut stream) = match client
.subscribe_with_request(Some(subscribe_request.clone()))
.await
{
Ok(subscription) => subscription,
Err(error) => {
let current_diagnostic = error.to_string();
first_diagnostic.get_or_insert_with(|| current_diagnostic.clone());
eprintln!("Subscribe error: {current_diagnostic}");
eprintln!("First diagnostic retained: {}", first_diagnostic.as_deref().unwrap());
return Err(error.into());
}
};
loop {
match stream.next().await {
Some(Ok(msg)) => {
retry_delay = Duration::from_secs(1);
if let Some(UpdateOneof::Slot(slot)) = msg.update_oneof {
tracked_slot = slot.slot;
println!(
"Slot: {} | Status: {:?}",
slot.slot,
SlotStatus::try_from(slot.status).unwrap()
);
}
}
Some(Err(error)) => {
let current_diagnostic = error.to_string();
first_diagnostic.get_or_insert_with(|| current_diagnostic.clone());
eprintln!("Stream error: {current_diagnostic}");
eprintln!("First diagnostic retained: {}", first_diagnostic.as_deref().unwrap());
return Err(error.into());
}
None => break,
}
}
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
}
}use anyhow::Result;
use futures::stream::StreamExt;
use std::{collections::HashMap, time::Duration};
use solana_stream_sdk::{
GeyserCommitmentLevel as CommitmentLevel, GeyserGrpcClient,
GeyserSlotStatus as SlotStatus, GeyserSubscribeRequest as SubscribeRequest,
GeyserSubscribeRequestFilterSlots as SubscribeRequestFilterSlots,
GeyserUpdateOneof as UpdateOneof,
};
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = "http://your-erpc-grpc-endpoint";
let mut subscribe_request = SubscribeRequest {
slots: HashMap::from([(
"slots".to_string(),
SubscribeRequestFilterSlots::default(),
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
};
let mut tracked_slot: u64 = 0;
let mut retry_delay = Duration::from_secs(1);
let max_retry_delay = Duration::from_secs(30);
let mut first_diagnostic: Option<String> = None;
loop {
let mut client = match GeyserGrpcClient::build_from_shared(endpoint)?.connect().await {
Ok(client) => client,
Err(error) => {
let current_diagnostic = error.to_string();
first_diagnostic.get_or_insert_with(|| current_diagnostic.clone());
eprintln!("Connection error: {current_diagnostic}");
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
continue;
}
};
subscribe_request.from_slot = if tracked_slot > 0 {
Some(tracked_slot)
} else {
None
};
let (mut _sink, mut stream) = match client
.subscribe_with_request(Some(subscribe_request.clone()))
.await
{
Ok(subscription) => subscription,
Err(error) => {
let current_diagnostic = error.to_string();
first_diagnostic.get_or_insert_with(|| current_diagnostic.clone());
eprintln!("Subscribe error: {current_diagnostic}");
eprintln!("First diagnostic retained: {}", first_diagnostic.as_deref().unwrap());
return Err(error.into());
}
};
loop {
match stream.next().await {
Some(Ok(msg)) => {
retry_delay = Duration::from_secs(1);
if let Some(UpdateOneof::Slot(slot)) = msg.update_oneof {
tracked_slot = slot.slot;
println!(
"Slot: {} | Status: {:?}",
slot.slot,
SlotStatus::try_from(slot.status).unwrap()
);
}
}
Some(Err(error)) => {
let current_diagnostic = error.to_string();
first_diagnostic.get_or_insert_with(|| current_diagnostic.clone());
eprintln!("Stream error: {current_diagnostic}");
eprintln!("First diagnostic retained: {}", first_diagnostic.as_deref().unwrap());
return Err(error.into());
}
None => break,
}
}
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
}
}这个保守的示例只会在建立连接失败和流正常关闭时重试。遇到 gRPC 订阅错误或流错误时,它会记录当前的诊断信息并保留第一条诊断信息,然后停止。手动重试之前,请先检查显示的 ERPC 修正说明以及服务器给出的任何重试信息。生产代码可以显式地重试已知的暂时性故障,但必须遵守服务器设定的暂停,且不得反复发送未作修改、已被拒绝的请求。
流式获取某个 mint 的账户(代币账户)
rust
SubscribeRequest {
accounts: HashMap::from([(
"token_accounts_by_mint".to_string(),
SubscribeRequestFilterAccounts {
owner: vec!["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string()],
filters: vec![
SubscribeRequestFilterAccountsFilter {
filter: Some(Filter::TokenAccountState(true)),
},
SubscribeRequestFilterAccountsFilter {
filter: Some(Filter::Memcmp(SubscribeRequestFilterAccountsFilterMemcmp {
offset: 0,
data: Some(Data::Base58(
"pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn".to_string(), // replace with mint
)),
})),
},
],
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
}SubscribeRequest {
accounts: HashMap::from([(
"token_accounts_by_mint".to_string(),
SubscribeRequestFilterAccounts {
owner: vec!["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string()],
filters: vec![
SubscribeRequestFilterAccountsFilter {
filter: Some(Filter::TokenAccountState(true)),
},
SubscribeRequestFilterAccountsFilter {
filter: Some(Filter::Memcmp(SubscribeRequestFilterAccountsFilterMemcmp {
offset: 0,
data: Some(Data::Base58(
"pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn".to_string(), // replace with mint
)),
})),
},
],
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
}流式获取交易(成功的、非投票的)
rust
SubscribeRequest {
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
}SubscribeRequest {
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
}生产级客户端要点
- 带重连和间隙恢复(
from_slot)的持久客户端 - Ping/pong 处理以保持流活跃
- 接收和处理之间使用有界通道/背压
- 在同一双向流上动态更新订阅
- 错误处理、指标和慢消费者/丢弃更新的日志记录
组合式客户端模式(间隙恢复、ping/pong、动态订阅)
此组合展示了有界接收与动态订阅。其接收任务只会在建立连接失败或流正常结束后等待并重新连接。gRPC 订阅错误或流错误会被报告并停止该任务,以便检查其修正说明与重试信息;这并不是通用的重试策略。
rust
use futures::{SinkExt, stream::StreamExt};
use solana_signature::Signature;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::{RwLock, mpsc};
use solana_stream_sdk::{
GeyserCommitmentLevel as CommitmentLevel, GeyserGrpcClient, GeyserSlotStatus,
GeyserSubscribeRequest as SubscribeRequest,
GeyserSubscribeRequestFilterSlots as SubscribeRequestFilterSlots,
GeyserSubscribeRequestFilterTransactions as SubscribeRequestFilterTransactions,
GeyserSubscribeUpdate as SubscribeUpdate, GeyserUpdateOneof as UpdateOneof,
};
use solana_stream_sdk::yellowstone_grpc_proto::geyser::SubscribeRequestPing;
const UPDATE_CHANNEL_CAPACITY: usize = 10_000;
#[tokio::main]
async fn main() {
let endpoint = "http://your-erpc-grpc-endpoint".to_string();
let subscribe_request = Arc::new(RwLock::new(SubscribeRequest {
slots: HashMap::from([(
"slots".to_string(),
SubscribeRequestFilterSlots {
..Default::default()
},
)]),
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
}));
let (updates_tx, updates_rx) = mpsc::channel::<SubscribeUpdate>(UPDATE_CHANNEL_CAPACITY);
let (subscribe_tx, subscribe_rx) = mpsc::unbounded_channel::<SubscribeRequest>();
let dispatcher_handle = tokio::spawn(async move {
dispatcher_loop(updates_rx).await;
});
let ingress_handle = tokio::spawn(async move {
ingress_loop(
updates_tx,
subscribe_rx,
endpoint,
subscribe_request,
)
.await;
});
if let Err(e) = tokio::join!(dispatcher_handle, ingress_handle).0 {
println!("Error: {:?}", e);
}
}
async fn ingress_loop(
updates_tx: mpsc::Sender<SubscribeUpdate>,
mut subscribe_rx: mpsc::UnboundedReceiver<SubscribeRequest>,
endpoint: String,
subscribe_request: Arc<RwLock<SubscribeRequest>>,
) {
let mut tracked_slot: u64 = 0;
let mut retry_delay = Duration::from_secs(1);
let max_retry_delay = Duration::from_secs(30);
loop {
let builder = match GeyserGrpcClient::build_from_shared(endpoint.clone()) {
Ok(builder) => builder,
Err(error) => {
eprintln!("Client configuration error: {error}");
return;
}
};
let mut client = match builder.connect().await {
Ok(client) => client,
Err(error) => {
eprintln!("Connection error: {error}");
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
continue;
}
};
subscribe_request.write().await.from_slot = if tracked_slot > 0 {
Some(tracked_slot)
} else {
None
};
match client
.subscribe_with_request(Some(subscribe_request.read().await.clone()))
.await
{
Ok((mut subscribe_tx, mut stream)) => {
loop {
tokio::select! {
subscription = subscribe_rx.recv() => {
match subscription {
Some(subscribe_request) => {
if let Err(error) = subscribe_tx.send(subscribe_request).await {
eprintln!("Subscription update error: {error}");
return;
}
}
None => return,
}
}
result = stream.next() => {
match result {
Some(Ok(update)) => {
retry_delay = Duration::from_secs(1);
if matches!(update.update_oneof, Some(UpdateOneof::Ping(_))) {
if let Err(error) = subscribe_tx
.send(SubscribeRequest {
ping: Some(SubscribeRequestPing { id: 1 }),
..Default::default()
})
.await
{
eprintln!("Ping response error: {error}");
return;
}
}
if matches!(update.update_oneof, Some(UpdateOneof::Ping(_)) | Some(UpdateOneof::Pong(_))) {
continue;
}
if let Some(UpdateOneof::Slot(ref slot_update)) = update.update_oneof {
tracked_slot = slot_update.slot;
continue;
}
if let Err(error) = updates_tx.send(update.clone()).await {
eprintln!("Update consumer stopped: {error}");
return;
}
}
Some(Err(error)) => {
eprintln!("Stream error: {error}");
return;
}
None => {
println!("Stream ended cleanly");
break;
}
}
}
}
}
}
Err(error) => {
eprintln!("Subscribe error: {error}");
return;
}
};
println!("Disconnected; retrying after bounded backoff...");
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
}
}
async fn dispatcher_loop(mut updates_rx: mpsc::Receiver<SubscribeUpdate>) {
loop {
if let Some(update) = updates_rx.recv().await {
match update.update_oneof {
Some(UpdateOneof::Transaction(tx)) => {
let sig = tx
.transaction
.and_then(|t| Signature::try_from(t.signature.as_slice()).ok())
.unwrap_or_default();
println!("Slot: {} | Signature: {}", tx.slot, sig);
}
_ => {
println!("Received update: {:?}", update);
}
}
}
}
}use futures::{SinkExt, stream::StreamExt};
use solana_signature::Signature;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::{RwLock, mpsc};
use solana_stream_sdk::{
GeyserCommitmentLevel as CommitmentLevel, GeyserGrpcClient, GeyserSlotStatus,
GeyserSubscribeRequest as SubscribeRequest,
GeyserSubscribeRequestFilterSlots as SubscribeRequestFilterSlots,
GeyserSubscribeRequestFilterTransactions as SubscribeRequestFilterTransactions,
GeyserSubscribeUpdate as SubscribeUpdate, GeyserUpdateOneof as UpdateOneof,
};
use solana_stream_sdk::yellowstone_grpc_proto::geyser::SubscribeRequestPing;
const UPDATE_CHANNEL_CAPACITY: usize = 10_000;
#[tokio::main]
async fn main() {
let endpoint = "http://your-erpc-grpc-endpoint".to_string();
let subscribe_request = Arc::new(RwLock::new(SubscribeRequest {
slots: HashMap::from([(
"slots".to_string(),
SubscribeRequestFilterSlots {
..Default::default()
},
)]),
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
}));
let (updates_tx, updates_rx) = mpsc::channel::<SubscribeUpdate>(UPDATE_CHANNEL_CAPACITY);
let (subscribe_tx, subscribe_rx) = mpsc::unbounded_channel::<SubscribeRequest>();
let dispatcher_handle = tokio::spawn(async move {
dispatcher_loop(updates_rx).await;
});
let ingress_handle = tokio::spawn(async move {
ingress_loop(
updates_tx,
subscribe_rx,
endpoint,
subscribe_request,
)
.await;
});
if let Err(e) = tokio::join!(dispatcher_handle, ingress_handle).0 {
println!("Error: {:?}", e);
}
}
async fn ingress_loop(
updates_tx: mpsc::Sender<SubscribeUpdate>,
mut subscribe_rx: mpsc::UnboundedReceiver<SubscribeRequest>,
endpoint: String,
subscribe_request: Arc<RwLock<SubscribeRequest>>,
) {
let mut tracked_slot: u64 = 0;
let mut retry_delay = Duration::from_secs(1);
let max_retry_delay = Duration::from_secs(30);
loop {
let builder = match GeyserGrpcClient::build_from_shared(endpoint.clone()) {
Ok(builder) => builder,
Err(error) => {
eprintln!("Client configuration error: {error}");
return;
}
};
let mut client = match builder.connect().await {
Ok(client) => client,
Err(error) => {
eprintln!("Connection error: {error}");
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
continue;
}
};
subscribe_request.write().await.from_slot = if tracked_slot > 0 {
Some(tracked_slot)
} else {
None
};
match client
.subscribe_with_request(Some(subscribe_request.read().await.clone()))
.await
{
Ok((mut subscribe_tx, mut stream)) => {
loop {
tokio::select! {
subscription = subscribe_rx.recv() => {
match subscription {
Some(subscribe_request) => {
if let Err(error) = subscribe_tx.send(subscribe_request).await {
eprintln!("Subscription update error: {error}");
return;
}
}
None => return,
}
}
result = stream.next() => {
match result {
Some(Ok(update)) => {
retry_delay = Duration::from_secs(1);
if matches!(update.update_oneof, Some(UpdateOneof::Ping(_))) {
if let Err(error) = subscribe_tx
.send(SubscribeRequest {
ping: Some(SubscribeRequestPing { id: 1 }),
..Default::default()
})
.await
{
eprintln!("Ping response error: {error}");
return;
}
}
if matches!(update.update_oneof, Some(UpdateOneof::Ping(_)) | Some(UpdateOneof::Pong(_))) {
continue;
}
if let Some(UpdateOneof::Slot(ref slot_update)) = update.update_oneof {
tracked_slot = slot_update.slot;
continue;
}
if let Err(error) = updates_tx.send(update.clone()).await {
eprintln!("Update consumer stopped: {error}");
return;
}
}
Some(Err(error)) => {
eprintln!("Stream error: {error}");
return;
}
None => {
println!("Stream ended cleanly");
break;
}
}
}
}
}
}
Err(error) => {
eprintln!("Subscribe error: {error}");
return;
}
};
println!("Disconnected; retrying after bounded backoff...");
tokio::time::sleep(retry_delay).await;
retry_delay = (retry_delay * 2).min(max_retry_delay);
}
}
async fn dispatcher_loop(mut updates_rx: mpsc::Receiver<SubscribeUpdate>) {
loop {
if let Some(update) = updates_rx.recv().await {
match update.update_oneof {
Some(UpdateOneof::Transaction(tx)) => {
let sig = tx
.transaction
.and_then(|t| Signature::try_from(t.signature.as_slice()).ok())
.unwrap_or_default();
println!("Slot: {} | Signature: {}", tx.slot, sig);
}
_ => {
println!("Received update: {:?}", update);
}
}
}
}
}贡献示例
发现了有用的模式?欢迎贡献到这些文档中。
Solana Stream SDK 仓库:https://github.com/ValidatorsDAO/solana-stream
Solana Stream SDK 仓库:https://github.com/ValidatorsDAO/solana-stream






