RPC transaction crawler
Backfill and then poll every transaction that mentions an address, using getSignaturesForAddress and getTransaction on any Solana RPC.
On this page
- Quick example
- Configuration
- RpcTransactionCrawler::
new - Filters
- ConnectionConfig
- RetryConfig
- Behaviour
- Troubleshooting
- Signatures are fetched but nothing is processed
- Method does not support commitment below confirmed
- Transactions are missing from a backfill
- Transactions arrive out of order
- The program never exits after the backfill
- See also
RpcTransactionCrawler pages through getSignaturesForAddress for one address, fetches each transaction with getTransaction, and emits it as a transaction update. It walks history backwards from the newest signature (or from a signature you choose), then keeps polling for new ones. Use it to backfill a program's or wallet's history, or to index a low-traffic program from any RPC endpoint, including the free public one.
| Crate | carbon-rpc-transaction-crawler-datasource 2.0.0 |
| Install | cargo add carbon-rpc-transaction-crawler-datasource@2. |
| Emits | Transaction |
| Mode | Historical backfill, then polling for new transactions. Never finishes on its own. |
| Provider | Any Solana JSON-RPC HTTP endpoint. The public https:/ works, slowly. |
| API docs | docs.rs |
| Source | datasources/ |
Quick example#
This program crawls the Orca Whirlpool program on the public RPC and prints every decoded swap.
cargo new crawler && cd crawler
cargo add carbon-core@2.0.0 carbon-log-metrics@2.0.0 carbon-orca-whirlpool-decoder@2.0.0 carbon-rpc-transaction-crawler-datasource@2.0.0
cargo add tokio@1 --features full
cargo add env_logger@0.11 log@0.4use {
carbon_core::{
error::CarbonResult, instruction::InstructionProcessorInputType, pipeline::Pipeline,
processor::Processor,
},
carbon_log_metrics::LogMetrics,
carbon_orca_whirlpool_decoder::{
instructions::OrcaWhirlpoolInstruction, OrcaWhirlpoolDecoder, PROGRAM_ID,
},
carbon_rpc_transaction_crawler_datasource::{ConnectionConfig, Filters, RpcTransactionCrawler},
std::sync::Arc,
};
#[tokio::main]
async fn main() -> CarbonResult<()> {
env_logger::init();
let datasource = RpcTransactionCrawler::new(
"https://api.mainnet-beta.solana.com".to_string(),
PROGRAM_ID, // crawl signatures that mention the Orca Whirlpool program
ConnectionConfig::default(), // 100 signatures per page, 5 concurrent getTransaction calls
Filters::new(None, None, None),
None, // commitment: defaults to confirmed
);
Pipeline::builder()
.datasource(datasource)
.metrics(Arc::new(LogMetrics::new()))
.instruction(OrcaWhirlpoolDecoder, SwapLogger)
.build()?
.run()
.await
}
struct SwapLogger;
impl Processor<InstructionProcessorInputType<'_, OrcaWhirlpoolInstruction>> for SwapLogger {
async fn process(
&mut self,
input: &InstructionProcessorInputType<'_, OrcaWhirlpoolInstruction>,
) -> CarbonResult<()> {
let tx = &input.metadata.transaction_metadata;
match input.decoded_instruction {
OrcaWhirlpoolInstruction::Swap { data, accounts, .. } => {
println!("slot {} swap pool={} amount={}", tx.slot, accounts.whirlpool, data.amount);
}
OrcaWhirlpoolInstruction::SwapV2 { data, accounts, .. } => {
println!("slot {} swapV2 pool={} amount={}", tx.slot, accounts.whirlpool, data.amount);
}
_ => {}
}
Ok(())
}
}cargo runslot 467115758 swap pool=4v8ufj8Hj7UvFgtofQJAtzUud5xomwZfEqfCTHZ4wM72 amount=19999
slot 467115758 swapV2 pool=FpCMFDFGYotvufJ7HrFHsWEiiQCGbkLCtwHiDnh7o28Q amount=7639572
slot 467115758 swap pool=Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE amount=2476330084
slot 467115758 swap pool=FpCMFDFGYotvufJ7HrFHsWEiiQCGbkLCtwHiDnh7o28Q amount=168756526The public endpoint rate-limits getTransaction per IP (its x-ratelimit-method-limit header reported 10 when this page was written), so expect a few transactions per second at most. Run with RUST_ to see the 429 retries and the pipeline counters. The program runs until you press Ctrl-C.
Configuration#
RpcTransactionCrawler:: new#
pub const fn new(
rpc_url: String,
account: Pubkey,
connection_config: ConnectionConfig,
filters: Filters,
commitment: Option<CommitmentConfig>,
) -> Self| Parameter | Type | Description |
|---|---|---|
rpc_url | String | HTTP JSON-RPC endpoint. |
account | Pubkey | Address passed to getSignaturesForAddress: a program ID, a wallet, a pool, any account. |
connection_ | ConnectionConfig | Paging, concurrency, retries and channel sizes. See below. |
filters | Filters | Optional account filter and signature range. See below. |
commitment | Option< | Commitment for both RPC calls. None means confirmed. processed is not accepted by these RPC methods. |
All fields of RpcTransactionCrawler are public, so you can also build it with a struct literal.
Filters#
pub const fn new(
accounts: Option<Vec<Pubkey>>,
before_signature: Option<Signature>,
until_signature: Option<Signature>,
) -> Self| Field | Type | Default | Meaning |
|---|---|---|---|
accounts | Option< | None | Keep only transactions whose account keys (static keys plus addresses loaded from lookup tables) include at least one of these. Applied after the transaction is fetched, so it saves processing, not RPC calls. |
before_ | Option< | None | Start the backfill immediately before this signature (exclusive) instead of at the newest one. |
until_ | Option< | None | Stop the backfill when this signature is reached (exclusive). |
ConnectionConfig#
ConnectionConfig:: is an inherent const fn, not the Default trait. Override single fields with ConnectionConfig { batch_. The positional constructor takes the arguments in a different order from the fields:
pub const fn new(
batch_limit: usize,
polling_interval: Duration,
max_concurrent_requests: usize,
retry_config: RetryConfig,
max_signature_channel_size: Option<usize>,
max_transaction_channel_size: Option<usize>,
blocking_send: bool,
) -> Self| Field | Type | Default | Meaning |
|---|---|---|---|
batch_limit | usize | 100 | limit for each getSignaturesForAddress page. RPC nodes cap it at 1000. |
polling_ | Duration | 5 s | Sleep after a page comes back empty, before polling for new signatures. |
max_ | usize | 5 | getTransaction calls in flight at once. |
retry_ | RetryConfig | RetryConfig:: | Backoff for both RPC calls. |
max_ | Option< | None (1000) | Buffer between the signature fetcher and the transaction fetcher. |
max_ | Option< | None (1000) | Buffer between the transaction fetcher and the step that sends to the pipeline. |
blocking_ | bool | false | false uses try_send into the pipeline channel and drops the update when it is full. true waits for space. Set true for backfills that must be complete. |
RetryConfig#
pub const fn new(max_retries: u32, initial_backoff_ms: u64, max_backoff_ms: u64, backoff_multiplier: f64) -> Self
pub const fn default() -> Self // 3, 1000, 10000, 2.0
pub const fn no_retry() -> Self // 0, 0, 0, 0.0| Field | Type | Default | Meaning |
|---|---|---|---|
max_retries | u32 | 3 | Retries after the first failed attempt. |
initial_ | u64 | 1000 | Delay before the first retry. |
max_ | u64 | 10000 | Upper bound on the delay. |
backoff_ | f64 | 2.0 | Factor applied to the delay after each retry. |
Behaviour#
- Pipeline. Three tasks run in sequence: a signature fetcher, a transaction fetcher that runs up to
max_concurrent_ requests getTransactioncalls (Base64 encoding,maxSupportedTransactionVersion: 1), and a processor that builds the update and sends it to the pipeline.consumereturns as soon as the tasks are spawned. - Lifecycle. The first pass pages backwards from
before_(or the newest signature) until the RPC returns an empty page orsignature until_is reached. The crawler then sleepssignature polling_and polls from the tip down to the newest signature it has seen, forever. It never finishes, so a pipeline that only has this datasource runs until Ctrl-C or until you cancel it (see shutdown). After the first pass it also picks up everything newer thaninterval before_.signature - Ordering. Signatures arrive newest first, and transactions are emitted in the order their
getTransactioncalls complete. Updates are not in slot order. Sort byslotdownstream if order matters. - What is skipped. Failed transactions (non-
Okstatus) and transactions without metadata are dropped. Every update hasis_,vote: false index: Noneandblock_;hash: None block_timecomes from the RPC. - Retries. Each call retries with exponential backoff from
RetryConfig. A signature page that still fails is retried from scratch on the next loop, so the crawler never skips a page. A transaction that still fails is logged withFailed to fetch transaction … after N retriesand skipped. On top of that, Solana'sRpcClientretries HTTP 429 responses up to 5 times, honouringRetry-After, and only logs atdebuglevel. - Backpressure. With
blocking_, updates are dropped withsend: false Failed to send updatewhen the pipeline channel (channel_, default 1000) is full.buffer_ size - Metrics.
transaction_,crawler_ signatures_ fetched_ total transaction_(counters),crawler_ transactions_ fetched_ total transaction_,crawler_ signatures_ fetch_ times_ milliseconds transaction_,crawler_ transaction_ fetch_ times_ milliseconds transaction_(histograms). See metrics.crawler_ transaction_ process_ time_ milliseconds
Troubleshooting#
Signatures are fetched but nothing is processed#
transaction_ grows while transaction_ stays at 0. At first nothing is logged, because RpcClient retries 429 responses silently; once those retries run out, RUST_ shows Failed to fetch transaction … Status(429, None). The RPC is rate limiting getTransaction. On the public endpoint the limit is shared by everyone on your IP. Lower max_ to 1 or 2, or use a dedicated RPC provider.
Method does not support commitment below confirmed#
You passed CommitmentConfig::. getSignaturesForAddress and getTransaction only accept confirmed and finalized; the signature fetcher retries the failing call forever.
Transactions are missing from a backfill#
Check the logs for Failed to send update (pipeline channel full) and Failed to fetch transaction … after N retries (RPC errors). Set blocking_, raise retry_, and raise the pipeline's channel_ if your processors are slow. Failed transactions are skipped by design.
Transactions arrive out of order#
Expected: getTransaction calls complete in any order. Use slot (and your own ordering) in the processor, or set max_ for strictly newest-first order.
The program never exits after the backfill#
Expected: the crawler switches to polling. To stop at the end of a range, cancel the pipeline from your code with datasource_, or use RpcBlockCrawler with an end_slot.
See also#
- Backfill transactions
- Stream with RPC
- Helius
getTransactionsForAddress: the same job in fewer calls, on Helius. - Datasources concept and update types