Geyser gRPC - Contoh Kode
Contoh Rust praktis untuk kasus penggunaan Geyser gRPC yang umum. Isi dengan endpoint Geyser gRPC Anda. Solana Stream SDK mendukung Geyser gRPC.
Subscription dasar: semua transaksi non-vote yang tidak gagal
Subscription transaksi ini sengaja dibuat luas. Mengecualikan transaksi yang gagal memang mengurangi traffic, tetapi stream yang tersisa tetap bisa besar. Tambahkan filter akun, signature, atau transaksi yang benar-benar dibutuhkan consumer Anda, dan jalankan pemrosesan di luar receive loop.
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(())
}Klien andal dengan reconnect terbatas setelah penutupan normal, pemulihan gap, dan 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);
}
}Contoh konservatif ini hanya mencoba ulang kegagalan saat membangun koneksi dan penutupan stream yang normal. Contoh ini berhenti pada error subscribe atau stream gRPC setelah mencatat diagnostik saat ini dan menyimpan diagnostik pertama. Periksa koreksi ERPC yang ditampilkan dan informasi retry dari server sebelum mencoba ulang secara manual. Kode produksi boleh secara eksplisit mencoba ulang kegagalan sementara yang sudah diketahui, tetapi harus menghormati jeda dari server dan tidak boleh berulang kali mengirim request yang ditolak tanpa mengubahnya.
Stream akun untuk sebuah mint (token account)
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()
}Stream transaksi (berhasil, non-vote)
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()
}Poin penting klien tingkat produksi
- Klien andal dengan reconnect dan pemulihan gap (
from_slot) - Penanganan ping/pong agar stream tetap hidup
- Bounded channel/backpressure antara ingress dan pemrosesan
- Pembaruan subscription dinamis pada stream bidi yang sama
- Penanganan error, metrik, dan logging untuk consumer yang lambat/update yang di-drop
Pola klien gabungan (pemulihan gap, ping/pong, subscription dinamis)
Komposisi ini menunjukkan ingress terbatas dan subscription dinamis. Task ingress-nya menunggu dan melakukan reconnect hanya setelah kegagalan membangun koneksi atau akhir stream yang normal. Error subscription atau stream gRPC dilaporkan dan menghentikan task agar koreksi dan informasi retry-nya dapat diperiksa; ini bukan kebijakan retry umum.
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);
}
}
}
}
}Berkontribusi dengan contoh
Menemukan pola yang berguna? Pertimbangkan untuk menyumbangkannya ke dokumentasi ini.
Repositori Solana Stream SDK: https://github.com/ValidatorsDAO/solana-stream
Repositori Solana Stream SDK: https://github.com/ValidatorsDAO/solana-stream






