Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

blockwatcher-stellar

blockwatcher-stellar is the Stellar chain family: one source, stellar-rpc, which polls stellar-rpc for closed ledgers inside the serving node’s history window, and the stellar decoder that source feeds (crates/blockwatcher-stellar/src/lib.rs). It is a module crate, built on blockwatcher-rpc’s endpoint pool and the stellar-xdr / stellar-strkey SDKs.

Stellar documents this family’s selector keys and source config at an operator’s level. This page maps the same facts onto the crate’s file layout: which file owns ingestion, which owns compile-once/decode-many, and the registry that wires both into the catalog.

Key takeaways

  • blockwatcher-stellar is the Stellar chain family: one source, stellar-rpc (closed ledgers inside the node’s RPC history window), plus the one stellar decoder it feeds.
  • Closed ledgers are not rewound. Event interest spends getEvents; operation or function interest spends getTransactions. On the first successful getHealth, getNetwork passphrases must agree across the pool or the run refuses by endpoint name.
  • A classic spec payload is {"catalog":"classic"}. A Stellar Asset Contract spec is {"catalog":"stellar-asset"}. A Soroban spec is a WASM blob or XDR-JSON of ScSpecEntry. Selector keys are events, functions, and operations.
  • A classic Payment and a CAP-67 transfer event on the same ledger are two decoded occurrences when both are selected; the decoder does not merge them.

Responsibilities

  • stellar-rpc (source::StellarRpcSource, source/): poll a pool of stellar-rpc endpoints for closed ledgers, refuse a configured start_ledger below oldestLedger on the first getHealth, clamp a checkpoint (or a subsequent retention floor) to the window with an explicit gap signal, and emit one RawEvent per matching occurrence (source/run.rs, source/fetch.rs, source/emit.rs).
  • Choose RPC methods from decoder interest: event interest spends getEvents (packed at five filters of five contract IDs; empty IDs send {type: contract}); operation or function interest spends getTransactions. Catch-up uses 10,000-ledger event chunks and narrows transaction windows to 512 ledgers (source/run.rs rustdoc, registry.rs). On first successful getHealth, probe_health calls getNetwork; disagreeing passphrases are SourceError::InvalidConfig naming endpoint names (source/fetch.rs).
  • Pack a ledger-primary cursor with a plane-and-index secondary so invocations, operations, and events in one ledger stay ordered (source/cursor.rs, source/emit.rs).
  • Translate StrKey addresses at the door into prefixed canonical bytes (decoder/address.rs); encode those bytes as 0x hex on the value tree.
  • Compile { "catalog": "classic" } into one schema per OperationType plus named host functions UploadContractWasm, CreateContract, and CreateContractV2 (decoder/classic.rs). InvokeHostFunction is a function_call; every other classic type is an operation.
  • Compile { "catalog": "stellar-asset" } from the pinned SEP-48 JSON through the Soroban compiler (decoder/stellar_asset.rs).
  • Compile a Soroban contract spec from WASM ({"wasm":"<base64>"}, reading the contractspecv0 custom section) or from XDR-JSON of an ScSpecEntry object or array (decoder/soroban.rs).
  • Materialize a spec payload from already-fetched getLedgerEntries JSON (spec_from_chain.rs); the decoder does not fetch.
  • Fetch a spec payload at write time from stellar-rpc getLedgerEntries (fetch_spec_from_rpc, spec_from_rpc.rs): a Stellar Asset Contract instance becomes { "catalog": "stellar-asset" }; a WASM contract becomes { "wasm": "<base64>" } that compile_spec accepts. A G-address is Invalid. An RPC failure is Unavailable and names endpoint names, never a URL. StellarSpecImporter is the SpecImporter adapter: it requires source.module to be stellar-rpc, builds that source’s pool from source.config, and calls fetch_spec_from_rpc.
  • Compile selector bodies that admit events, functions, operations, and addresses (decoder/selector.rs). Naming any of the three kind keys selects only what it names; naming none selects every declaration the referenced spec exposes.
  • Decode a source-owned JSON envelope whose xdr field is base64 XDR (decoder/envelope.rs, decoder/decode.rs). block.timestamp is envelope.close_time. tx.status is envelope.status. Malformed XDR is undecodable; a foreign compiled selector or a non-stellar chain is no_match.

Not this crate’s job: deciding which module a network or spec names (blockwatcher-embed’s catalog fold, the binary’s config); serving HTTP or metrics (blockwatcher-api, blockwatcher-metrics); evaluating predicates (blockwatcher-expr); knowing EVM types (blockwatcher-evm).

Key types and traits

NameKindRole
StellarRpcSourcestructThe Source implementation: closed-ledger polling over Pool<StellarEndpoint> (source/run.rs)
StellarRpcConfigstructBoot-time config: endpoints, url_secret, selection, timeouts, start cursor (source/config.rs)
StellarEndpointstructOne stellar-rpc JSON-RPC target the pool drives (source/jsonrpc.rs)
StellarDecoderstructThe Decoder implementation: claims chain stellar (decoder/mod.rs)
materialize_specfnLibrary writer: ledger-entry JSON → {wasm} or {catalog: stellar-asset} (spec_from_chain.rs)
fetch_spec_from_rpcfnWrite-time fetch: C-strkey → {wasm} or {catalog: stellar-asset} via getLedgerEntries; G-addresses are Invalid; RPC failure is Unavailable without echoing http (spec_from_rpc.rs)
StellarSpecImporterstructSpecImporter adapter: requires stellar-rpc, builds the pool from source.config, calls fetch_spec_from_rpc (spec_from_rpc.rs)
spec_importers::get_allfnThe family’s spec-importer table, keyed by chain; embed folds it the same way it folds sources and decoders (spec_from_rpc.rs)
Envelope, Planestruct/enumSource-owned JSON wrapper around base64 XDR plus status; plane is event / operation / invocation (decoder/envelope.rs)
StellarIntereststructKind flags and addresses the source downcasts from InterestSet::chain_specific (decoder/interest.rs)
StellarRpcRegistry, StellarDecoderRegistrystructModuleRegistry entries named "stellar-rpc" and "stellar" (registry.rs)

How data flows through it

flowchart LR
    health["getHealth<br/>retention window"] --> fetch["fetch_window"]
    fetch -->|"event interest"| events["getEvents<br/>sharded"]
    fetch -->|"operation or function interest"| txs["getTransactions"]
    events --> emit["emit_window<br/>Envelope + cursor"]
    txs --> emit
    emit --> decode["StellarDecoder::decode<br/>base64 XDR"]
    decode --> value["canonical DecodedEvent"]

A configured start_ledger below the node’s retained ledgers on the first health read is refused, naming both bounds. A checkpoint below that window clamps to the new oldest ledger, increments a retention gap counter, and continues — the same path a mid-run floor advance takes. If oldestLedger advances past the next ledger after that read, the run warns with the skipped range, increments a retention gap counter, publishes Degraded, and continues at the new oldest ledger. An inconsistent page publishes Degraded and retries the same window; it does not start at ledger 1. Invalidated is never returned for confirmation depth: closed ledgers are not rewound (source/run.rs). Resume trusts cursor.primary because a closed ledger is final: there is no hash-walk against live headers.

stellar-rpc

Trade-off: this module walks closed ledgers inside the serving node’s RPC history window. It misses nothing the node still retains, and spends RPC quota on getEvents / getTransactions proportional to interest and shard count. Ledgers the node has already dropped are unreachable. A restart resumes from a ledger the node still has; it cannot reconstruct history the node no longer serves.

scan and the live loop both walk wide ranges without truncation in bounded chunks. Event-only catch-up uses at most 10,000 ledgers per chunk. Any chunk that fetches transactions uses at most 512 ledgers. Pagination stops on an empty page, an item beyond the inclusive window end, or a page shorter than the requested limit. getTransactions does not send endLedger; the local inclusive bound still stops pagination. FAILED envelopes are emitted with Envelope.status copied from getTransactions. stellar-rpc does not implement confirmed_tip (the Source port default is Unsupported), so skip { "to": "tip" } is 422 invalid_resource with "this source cannot report a confirmed tip", the same as evm-mempool. Skip to a ledger/block number still works while paused.

The stellar decoder

compile_spec accepts these payload shapes (decoder/mod.rs):

  • {"catalog":"classic"} — one schema per classic OperationType, plus UploadContractWasm, CreateContract, and CreateContractV2.
  • {"catalog":"stellar-asset"} — baked Stellar Asset Contract spec (Transfer and TransferMuxed share the transfer topic).
  • {"wasm":"<base64>"} — Soroban contractspecv0.
  • XDR-JSON of an ScSpecEntry object or array.

An unknown catalog string is SpecError::Invalid.

compile requires a spec and admits only events, functions, operations, and addresses. InvokeHostFunction and the named host functions are selected through functions, never through operations. An explicit empty array for a kind list is SelectorError::Invalid.

decode matches envelope.plane, reads the base64 XDR, and looks up the compiled tables. EventPlan stores data_format. A selected classic Payment and a selected CAP-67 transfer event on the same ledger are two DecodedEvents; the decoder does not fold them into one occurrence. Two spec events that share the on-chain topic transfer (scalar vs muxed data) compile as distinct schema names; selecting transfer includes both plans (decoder/decode.rs, decoder/soroban.rs). materialize_spec walks LedgerEntryData XDR from fetched JSON (spec_from_chain.rs). Write-time import is fetch_spec_from_rpc plus StellarSpecImporter (spec_from_rpc.rs); the decoder still does not fetch. A cargo-fuzz target lives in crates/blockwatcher-stellar/fuzz/ (scval_to_value).

Addresses in selector bodies are StrKey at the door (decoder/address.rs): account, contract, and muxed flavors become prefixed canonical bytes so they cannot collide on the value tree.

The registry

Two ModuleRegistry implementations (registry.rs):

RegistryNAMEFactory builds
StellarRpcRegistry"stellar-rpc"Deserializes StellarRpcConfig, validates it, builds Pool<StellarEndpoint>, constructs StellarRpcSource::new(pool, config)
StellarDecoderRegistry"stellar"Accepts null or empty-object config and returns Arc::new(StellarDecoder)

sources::get_all() returns Vec<SourceModule> (SourceModule::of::<StellarRpcRegistry>()); decoders::get_all() stays (name, factory) (registry.rs, re-exported at the crate root). Those are what blockwatcher-embed’s build_catalog folds under #[cfg(feature = "stellar")].

Neighbours

blockwatcher-stellar depends on, in production:

  • blockwatcher-types, blockwatcher-ports, blockwatcher-rpc
  • stellar-xdr, stellar-strkey, wasmparser, base64
  • reqwest, url, serde, serde_json, thiserror
  • async-trait, tokio, tokio-util, metrics, tracing, num-bigint

and, in [dev-dependencies] only:

blockwatcher-embed depends on this crate when the stellar feature is on. Nothing in the core ring does.

Reading the source

  1. lib.rs: crate doc, decoder / registry / source, re-exports.
  2. registry.rs: the two ModuleRegistry impls and the sources / decoders get_all() tables.
  3. source/config.rs, source/jsonrpc.rs, source/fetch.rs, source/emit.rs, source/run.rs.
  4. decoder/envelope.rs, decoder/address.rs, decoder/classic.rs, decoder/soroban.rs, decoder/selector.rs, decoder/decode.rs.
  5. spec_from_chain.rs (materialize_spec), spec_from_rpc.rs (fetch_spec_from_rpc, StellarSpecImporter, spec_importers::get_all).