Stellar
The Stellar family is one source,
stellar-rpc, and the stellar
decoder it feeds
(crates/blockwatcher-stellar).
The crate page maps the
same facts onto file layout. This page is the operator surface: modules,
selector keys, spec payloads, source config, and what the stellar
feature folds.
Key takeaways
- Selector keys are
events,functions, andoperations. Naming any of them selects only what it names; naming none selects every declaration the referenced spec exposes of each kind. InvokeHostFunctionis afunction_call, named underfunctions. So areUploadContractWasm,CreateContract, andCreateContractV2. Classic operations such asPaymentare named underoperations.- 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 ofScSpecEntry. stellar-rpcwalks closed ledgers inside the node’s RPC history window. The first health read refuses a configuredstart_ledgerbelowoldestLedger. A resume whose checkpoint sits below the window clamps to the new oldest ledger with an explicit gap signal, the same as if retention advances during a run. Closed ledgers are not rewound. On the first successfulgetHealth, every non-open endpoint is probed withgetNetwork; disagreeing passphrases refuse the run by endpoint name. Event interest spendsgetEvents; operation or function interest spendsgetTransactions.- A classic
Paymentand a CAP-67transferevent on the same ledger are two decoded occurrences when both are selected; the decoder does not merge them. Selectingevents: ["transfer"]compiles both the scalartransferplan (barei128data) andMuxedTransfer(map{to_muxed_id, amount}) when the spec declares both. - Network
source.configis this family’sstellar-rpcshape, not a shared schema.
Modules
| Family | Module | Registered in | Trade-off |
|---|---|---|---|
| source | stellar-rpc | crates/blockwatcher-stellar/src/registry.rs | Walks closed ledgers inside the node’s RPC history window; misses nothing the node still retains, and spends RPC quota on getEvents/getTransactions proportional to interest and shard count. Closed ledgers are not rewound. Cold-starts from start_ledger and reports no confirmed tip. |
| decoder | stellar | crates/blockwatcher-stellar/src/registry.rs | Compiles a classic operation catalog, the baked Stellar Asset Contract catalog, or a Soroban contract spec (WASM or XDR-JSON) into schemas once per spec write; selector keys are events, functions, and operations. |
The stellar Cargo feature on blockwatcher-embed and the blockwatcher
binary folds those two modules. Turning it off removes them from
build_catalog; turning it off leaves every other family’s modules in
place.
stellar-rpc declares event timestamps: block.timestamp is
envelope.close_time (unix seconds), so a time-window
gate can compile against it. Skip { "to": "tip" } is
422 invalid_resource with "this source cannot report a confirmed tip"
because this source has no confirmed_tip, the same as evm-mempool.
Skip to a ledger/block number still works while paused.
Spec payload
A spec whose chain is stellar is one of:
{"catalog":"classic"}— every classicOperationType, compiled indecoder/classic.rs.InvokeHostFunctionbecomes afunction_call; every other classic type is anoperation. Named host functionsUploadContractWasm,CreateContract, andCreateContractV2are alsofunction_callschemas on this catalog.{"catalog":"stellar-asset"}— the baked Stellar Asset Contract spec (decoder/stellar_asset.rs), includingTransferandTransferMuxed.{"wasm":"<base64>"}— the contract’scontractspecv0custom section, compiled indecoder/soroban.rs.- XDR-JSON of an
ScSpecEntryobject or array — the same Soroban compiler, without WASM.
The decoder does not fetch a spec from chain. Operators supply a payload
on PUT /specs/{id} or in a seed file, or they produce one at write time
with POST /specs/{id}/from-chain or blockwatcher spec-import. Those
paths call getLedgerEntries, then materialize_spec: a Stellar Asset
Contract instance becomes { "catalog": "stellar-asset" }; a WASM
contract becomes { "wasm": "<base64>" } that compile_spec accepts.
Account (G) addresses are refused as invalid. A JSON-RPC permanent
refusal (-32602..=-32600) is invalid. Other RPC failures are
unavailable and name endpoint names, never a URL. After import, the
stored spec is the same {id, chain, payload} shape as a hand-written
one. blockwatcher_stellar::materialize_spec still turns already-fetched
getLedgerEntries JSON into that payload without performing RPC itself.
Addresses in a selector are StrKey at the door and become prefixed
canonical bytes on the value tree (decoder/address.rs), encoded as 0x
hex. Shared address rules stay on
Chain-agnosticism. The dashboard’s match JSON
keeps that hex and adds an explorer caption: flavor 0x00/0x01/0x02
becomes a G/C/M StrKey link on StellarExpert, and tx.hash opens the
transaction by unprefixed hash.
Selector keys
The compiler lives in crates/blockwatcher-stellar/src/decoder/selector.rs.
A selector body admits events, functions, operations, and
addresses; any other key is rejected by name. The three kind keys share
one presence matrix: naming any of them selects only what it names; naming
none selects every declaration the referenced spec exposes of each kind.
An explicit empty array for a kind list or for addresses is rejected
rather than treated as absence.
InvokeHostFunction is a function_call. So are UploadContractWasm,
CreateContract, and CreateContractV2. A selector that wants those
invocations names them under functions, not under operations. Classic
operations such as Payment are named under operations.
Shared selector contracts (spec reference, OR across entries, unknown keys rejected by the family’s compiler) stay on Selectors.
Source
stellar-rpc polls stellar-rpc for closed ledgers inside
oldestLedger..=latestLedger from getHealth. On the first health read, a
configured start_ledger below oldestLedger is refused. A checkpoint
below that window clamps to the new oldest ledger with a retention gap. If
oldestLedger advances past the next ledger during a run, the source reports
the skipped range as a retention gap and clamps the next fetch to the new
oldest ledger. Catch-up walks event ranges in chunks of at most 10,000
ledgers. Transaction interest narrows each chunk to at most 512 ledgers so
returned envelopes remain bounded in memory.
Cursor primary is ledger sequence; secondary orders invocation, then
operation, then event within the ledger (source/cursor.rs,
source/emit.rs). journal_depth is denominated in those primary units.
The source chooses RPC methods from decoder interest: event interest
spends getEvents (packed at five filters of five contract IDs; an empty
address set sends {type: "contract"});
operation or function interest spends getTransactions
(source/run.rs). On the first successful getHealth, every non-open
endpoint is probed with getNetwork; disagreeing passphrases refuse the
run by endpoint name, never echoing a URL. The envelope on the wire is source-owned JSON wrapping
base64 XDR (decoder/envelope.rs). Decoded occurrences carry tx.status
(SUCCESS or FAILED from getTransactions; contract events from
getEvents carry SUCCESS). Predicates filter; the source does not drop
FAILED. Malformed XDR is undecodable; a foreign compiled selector or a
non-stellar chain is no_match.
Source config
The network resource envelope (id, chain, source.module) is on
Network. The tables below are this
family’s source.config.
Endpoints: the pool vocabulary
stellar-rpc names a pool of HTTP JSON-RPC endpoints
(EndpointDef, crates/blockwatcher-stellar/src/source/config.rs). There
is no priority key; every endpoint is constructed as the pool’s high
tier (registry.rs). Each endpoint object:
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | "primary" | Labels every metric and log line for this endpoint. A repeat refuses: endpoint name 'primary' is used by more than one endpoint; endpoint names must be unique. Two endpoints that omit name both become primary and refuse for that reason. |
url_secret | string | none (required) | An env:NAME reference to where the URL lives, never the URL itself. A value that is not a reference refuses with endpoint '<name>' url_secret is not a secret reference: ..., deliberately never echoing what was written: a provider URL routinely carries an API key. The resolved value must be http or https; any other scheme is refused at write, naming the scheme and never the URL. |
rate_limit | object { "rps": u32 } | none | A preemptive request rate enforced before a call leaves the pool. A zero refuses: endpoint '<name>' rate_limit.rps is 0; it must be at least 1. |
weight | u32 | 1 | Ring slots in the pool’s rotation; meaningful only under round_robin selection, inert under ordered. A zero refuses: endpoint '<name>' weight is 0; it must be at least 1. Above the pool’s cap of 100 (blockwatcher_rpc::MAX_WEIGHT) refuses: endpoint '<name>' weight (101) exceeds the maximum (100). |
stellar-rpc
Polls closed ledgers over HTTP JSON-RPC: events via getEvents, and,
while some monitor watches operations or functions, transactions via
getTransactions. Its config (StellarRpcConfig,
crates/blockwatcher-stellar/src/source/config.rs) rejects unrecognized
fields, including a network key: which network a source’s events belong
to is the engine’s to supply, never the config’s. There is no
confirmations key: closed ledgers are not rewound, so this source never
returns Invalidated for confirmation depth (source/run.rs).
The module’s complete example config, registry_examples/stellar_rpc.json
from crates/blockwatcher-stellar/, verbatim, the same file the crate’s
family-completeness test constructs:
{
"start_ledger": 1,
"poll_interval_ms": 2000,
"endpoints": [
{ "url_secret": "env:STELLAR_RPC_URL", "weight": 1 }
]
}
start_ledger: 1 is a valid construct-time value — the floor the schema
accepts, and the number the family-completeness test constructs. A public
node’s oldestLedger sits far above 1, so this same JSON refuses at the
first getHealth unless the operator stamps a ledger inside
oldestLedger..=latestLedger. The runnable seed under
examples/source-stellar-rpc-monitor does that from getHealth; the
file-by-file pass is Monitoring Stellar Testnet
[RPC].
The tunables:
| Key | Type | Default | Meaning |
|---|---|---|---|
endpoints | array | none (required) | The pool, per the table above. An empty array refuses: stellar-rpc requires at least one endpoint. |
selection | string: ordered or round_robin | "ordered" | How the pool orders endpoints when no window pin dictates the choice: ordered concentrates calls on the first admissible endpoint in configuration order; round_robin spreads windows across the pool, weighted by each endpoint’s weight. A misspelling is refused by serde naming both accepted spellings. |
start_ledger | u32 | required | Where a run with no persisted checkpoint begins scanning. Deliberately absolute: a head-relative start would re-derive a different ledger on every restart and silently skip the gap. A missing key refuses at construct. A zero refuses: start_ledger (0) must be at least 1. If this value is below oldestLedger on the first health read, the run refuses it and names oldestLedger and latestLedger. A checkpoint below that window clamps and records a gap instead. If retention advances past the next ledger after that read, the source records the skipped range as a gap and continues from the new oldestLedger. |
poll_interval_ms | u64 | 2000 | How often, while caught up, one getHealth check advances the emission barrier. The same interval paces a retry after a failed getHealth or window fetch. A zero refuses: poll_interval_ms (0) must be at least 1. |
request_timeout_ms | u64 | 10000 | The per-attempt HTTP timeout the pool enforces. A zero refuses: request_timeout_ms (0) must be at least 1. |