Monitoring Stellar Testnet [RPC]
Your first monitor walks the EVM
Sepolia example end to end. This page is the same kind of pass for
examples/source-stellar-rpc-monitor/: every seed file, annotated, as a
reference for what each line does. Run that directory’s README.md first
if you have not started the pipeline yet; read this one when you want to
know exactly what you are looking at.
source-stellar-rpc-monitor/
├── .env.example
├── blockwatcher.toml
├── setup.sh
└── resources/
├── networks/stellar-testnet.json
├── specs/classic.json
├── specs/native-xlm.json
├── sinks/log-sink.json
├── monitors/xlm-payments.json
├── monitors/xlm-transfers.json
└── monitors/xlm-transfer-calls.json
The seed watches three things on one stellar-rpc network: classic native
XLM Payment operations, native SAC transfer events, and native SAC
transfer invocations. A classic Payment and a CAP-67 transfer event
on the same ledger are two decoded occurrences; the decoder does not merge
them. Selector keys and spec payloads for this family are on
Stellar.
.env.example
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
BLOCKWATCHER_API_TOKEN=local-test-token
Two variables, both required. Nothing under resources/ ever holds
STELLAR_RPC_URL’s actual value: resources/networks/stellar-testnet.json
below names the variable itself, and blockwatcher resolves it from its own
process environment the moment it needs it. The public SDF Testnet node
needs no API key; swap the URL if you run your own. BLOCKWATCHER_API_TOKEN
can be any string here: the example’s blockwatcher.toml binds the API to
127.0.0.1 only.
blockwatcher.toml
[api]
enabled = true
listen = "127.0.0.1:8081"
[[auth.tokens]]
label = "operator"
scope = "admin"
secret = "env:BLOCKWATCHER_API_TOKEN"
[metrics]
enabled = true
listen = "127.0.0.1:9091"
[storage]
module = "sqlite"
config = { path = "blockwatcher.db" }
[engine]
event_channel_capacity = 1000
sink_channel_capacity = 100
drain_deadline_ms = 5000
matcher = { module = "expr", config = {} }
Every section here is covered generally on the Configuration
reference. This instance listens on 8081 / 9091 so
it can run beside examples/source-evm-rpc-monitor on 8080 / 9090.
[storage].config.path is a bare relative filename, blockwatcher.db, so
the database lands next to the process’s current directory: inside
examples/source-stellar-rpc-monitor/ if you cd there first.
setup.sh
start_ledger is absolute and required. The committed network JSON uses
1, which is a valid construct-time value (the floor the schema accepts)
and the same number registry_examples/stellar_rpc.json constructs. A
public node’s oldestLedger sits far above 1, so boot refuses that
placeholder at the first getHealth. The script asks the endpoint for
oldestLedger and latestLedger and stamps a ledger just behind the
head, clamped into that window.
resources/networks/stellar-testnet.json
{
"id": "stellar-testnet",
"chain": "stellar",
"source": {
"module": "stellar-rpc",
"config": {
"start_ledger": 1,
"endpoints": [
{
"name": "primary",
"url_secret": "env:STELLAR_RPC_URL",
"rate_limit": { "rps": 5 }
}
],
"poll_interval_ms": 2000,
"request_timeout_ms": 10000
}
}
}
chain is stellar; source.module is stellar-rpc. There is no
confirmations key: closed ledgers are not rewound. url_secret is an
env:NAME reference, never the URL. After ./setup.sh, start_ledger is
a ledger inside the node’s retention window rather than 1.
resources/specs/classic.json
{
"id": "classic",
"chain": "stellar",
"payload": { "catalog": "classic" }
}
The built-in catalog of every classic OperationType.
InvokeHostFunction becomes a function_call; every other classic type,
including Payment, is an operation.
resources/specs/native-xlm.json
The payload is XDR-JSON of ScSpecEntry: a CAP-67 transfer event
(from / to on the topic list, amount as scalar data) and a
transfer(from, to, amount) function. The native SAC id is not in the
spec; it lives on the monitors that select this spec.
resources/sinks/log-sink.json
One JSON line per match to stdout, identical to the Sepolia example’s log sink.
resources/monitors/
xlm-payments.json selects classic Payment with no address list and
keeps native-asset payments (args.asset == "native"). Omitting
addresses means “every address”; interest-union then blanks address
filters for the whole pipeline, so getEvents is unfiltered and the
decoder still drops everything that is not a selected transfer on the
native SAC. Add a G-address to this monitor if you want event fetches
restricted to the SAC id.
xlm-transfers.json selects events: ["transfer"] on
CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC.
xlm-transfer-calls.json selects functions: ["transfer"] on the same
id. Operation or function interest spends getTransactions (512-ledger
windows); event interest spends getEvents.
Addresses in the seed are StrKey. Decoded args.from / args.to on the
match JSON are 0x hex of prefixed canonical bytes.
amount.int is stroops (10,000,000 is 1 XLM).
Running
From examples/source-stellar-rpc-monitor/:
cp .env.example .env
./setup.sh
set -a; . ./.env; set +a
cargo run --bin blockwatcher -- check ./resources
cargo run --bin blockwatcher -- --config ./blockwatcher.toml --seed ./resources
check constructs every module the seed references, including resolving
url_secret, so STELLAR_RPC_URL has to be set to a real http/https
URL. The check inspects the scheme; the endpoint does not need to be
reachable. After boot, GET /status on 127.0.0.1:8081 is the operator
surface; see the example README.md for curl and jq.