Skip to main content

Crate satd_events_client

Crate satd_events_client 

Source
Expand description

Async Rust client SDK for the satd Streaming Consumption API (satd.events.v1).

It wraps the generated tonic client with a typed event model, auth metadata injection, and cursor capture, so a consumer writes against StreamClient instead of hand-rolling channels, metadata, and protobuf unwrapping.

use satd_events_client::{StreamClient, SubscribeOptions, Categories, Event};

let mut client = StreamClient::builder("http://127.0.0.1:50051")
    // Loopback, so the token never leaves the host. Off-host, use
    // `.bearer_token(..)` with an `https://` endpoint — it refuses to send
    // a credential over an unencrypted connection.
    .insecure_bearer_token("…")
    .keepalive_default()
    .connect()
    .await?;

let mut events = client.subscribe(SubscribeOptions {
    categories: Categories::MEMPOOL | Categories::CHAIN,
    ..Default::default()
}).await?;

while let Some(event) = events.message().await? {
    match event {
        Event::BlockConnected { height, .. } => println!("block {height}"),
        Event::MempoolEnter { fee, vsize, .. } => println!("tx {fee}/{vsize}"),
        Event::Lagged { resume_cursor, .. } => {
            // reconnect with `resume_cursor` to recover the gap
            let _ = resume_cursor;
        }
        _ => {}
    }
}

§Scope (this revision)

  • StreamClient::subscribe — the full firehose with from_cursor replay and cursor capture.

  • StreamClient::watch — opens the bidirectional stream and returns a WatchHandle with typed helpers for every watch kind: add_scripts (with per-script min_value floors), add_outpoints, add_tx_lifecycle / add_depth_alarms, add_descriptor, and add_script_prefixes (the privacy-preserving prefix watch), plus set_categories / set_cursor and the matching remove_*. send_control remains for raw access.

  • StreamClient::resilient_subscribe — the firehose wrapped in a ResilientSubscription that reconnects with backoff, persists and replays the durable cursor (via a CursorStore), recovers from Lagged per a LagPolicy, and surfaces replay-truncation gaps.

  • StreamClient::resilient_watch — the bidirectional Watch stream wrapped in a ResilientWatch that mirrors the watch-set and re-registers it on reconnect (watch-sets are per-connection), then re-anchors off the deterministic Event::CursorAccepted / Event::CursorRejected results, retrying transient rejects in place and surfacing the rest for a resnapshot. Integrators with a durable watch-set source-of-truth can install a watch_set_loader to rebuild the canonical set from that truth on every (re)connect.

  • PrefixWatcher (default-on bitcoin feature) — the privacy-preserving prefix-watch local re-filter: decodes a PrefixMatch’s raw_tx and recomputes sha256(scriptPubKey) to keep only true matches.

§TLS / mTLS (default-on tls feature)

Builder methods encrypt the gRPC transport so a bearer token (and the event stream) never travels in cleartext:

  • tls — TLS with the bundled Mozilla roots (public-CA servers).
  • tls_ca_pem — pin a private / self-signed CA, the usual case for a satd node serving its own certificate.
  • tls_client_identity — present a client certificate for mutual TLS (-eventsgrpcmtls).
  • tls_domain — override the verified certificate name (SNI) when connecting by IP or through a proxy.
let ca = std::fs::read("node-ca.pem")?;
let client = StreamClient::builder("https://node.example:50051")
    .tls_ca_pem(ca)
    .bearer_token("…")
    .connect()
    .await?;

TLS uses the ring rustls provider. Opt out with default-features = false for a plaintext-only build — but note that such a build cannot encrypt anything: tonic only honours the https:// scheme when it too is compiled with TLS, so without this feature an https:// endpoint connects in cleartext. bearer_token therefore refuses every endpoint in a build without tls. The raw wire types are re-exported under proto for low-level use.

§Stability & versioning

The crate version is the satd version it shipped with, and follows semver. An SDK works with any node on the same event schema whose version is at or above its own; newer nodes only add fields and event kinds. When a stream opens, the SDK compares the node’s advertised version with its own: one minor version behind logs a tracing warning, two or more (or a major version) behind is refused with StreamError::NodeTooOld unless the client was built with allow_old_node, and a different schema is always refused with StreamError::SchemaMismatch. A node older than 0.6.0 advertises nothing and counts as 0.5. See StreamClient::node_version. The generated wire types are re-exported under proto so you can pin to the schema directly when a typed helper does not yet cover your case. Minimum supported Rust version (MSRV) is 1.93; an MSRV bump is treated as a minor-version change.

See the wire/streaming spec for the underlying gRPC contract and the satd-events-proto crate for the generated types.

Modules§

proto
The generated satd.events.v1 wire types, for low-level control-message construction until typed helpers cover every case.

Structs§

Backoff
Exponential reconnect backoff. Delays grow initial * multiplier^attempt, capped at max. No jitter is applied (a single client reconnecting to a single node needs none); add it externally if fanning many clients at one server.
Categories
Category bitfield helpers for SubscribeOptions::categories.
Cursor
Durable resume position, re-exported from the wire schema. Persist the value returned by EventStream::cursor and present it again as from_cursor to resume. Durable resume position. Confirmed-side cursors are (height, tx_index) — per-transaction, so a client can resume mid-block after a disconnect. mempool_seq is a best-effort high-water mark for the mempool side (advisory; resets on daemon restart). A client persists the cursor from the last NodeEvent it durably processed and presents it as from_cursor on reconnect.
DescriptorMatch
Descriptor attribution for a ScriptMatched: which descriptor watch a matched scripthash belongs to, and the exact coordinate the server derived it at.
EventStream
A live stream of typed Events, shared by Subscribe and Watch.
FileCursorStore
A CursorStore backed by a single file, written atomically (temp file + rename) so a crash mid-write never leaves a torn cursor. The on-disk format is one line of four whitespace-separated integers — height tx_index mempool_seq instance_id — stable and trivially inspectable. A missing file loads as None.
FundingHitbitcoin
An output (funding side) of a delivered tx that pays a watched script.
NoopCursorStore
A CursorStore that persists nothing: load is always None, save is a no-op. The default — reconnects still resume from the in-memory last cursor, but a process restart starts forward-only.
Outpoint
An outpoint (txid:vout), raw bytes.
PrefixHitsbitcoin
The result of re-filtering one PrefixMatch against the watched set: the outputs and spent prevouts that are genuine matches, plus any spend-side prevouts the server did not retain and so could not be filtered locally.
PrefixMatch
A transaction that fell inside a watched script-prefix bucket. Carries the full serialized tx so the client filters the bucket against its real scripts locally (the privacy contract — the server only learns the bucket).
PrefixWatcherbitcoin
Holds the consumer’s real scriptPubKeys and re-filters coarse prefix-bucket deliveries down to true matches.
RejectedDescriptor
A descriptor named by a WatchAddRejected.
ReloadSummary
The outcome of ResilientWatch::reload.
ResilientConfig
Bundles the resilience knobs for StreamClient::resilient_subscribe.
ResilientSubscription
A firehose that reconnects, replays from a persisted cursor, and recovers from lag on the consumer’s behalf. Construct it with StreamClient::resilient_subscribe and drive it by calling next in a loop.
ResilientWatch
A Watch stream that reconnects, re-registers its watch-set, and re-anchors off the deterministic Event::CursorAccepted / Event::CursorRejected results on the caller’s behalf.
ResilientWatchConfig
Knobs for StreamClient::resilient_watch. Reuses the Backoff and CursorStore from the ResilientSubscription resilience layer.
ScriptPrefix
A k-bit prefix of sha256(scriptPubKey).
SilentPaymentTarget
A BIP 352 scan-key watch target (Tier 2): a (scan_secret, spend_pubkey) pair plus optional labels, registered via WatchHandle::add_silent_payments. The node runs the ECDH match and pushes Event::SilentPaymentMatched.
SpendingHitbitcoin
A spent prevout (spending side) of a delivered tx that consumed a watched script. Only produced for prevouts the server retained the script for; unretained ones surface in PrefixHits::unresolved.
SpentPrevout
A spent prevout that matched a prefix bucket (the spend side of a Event::PrefixMatched).
StreamClient
An async client for the satd satd.events.v1 streaming API.
StreamClientBuilder
Builder for a StreamClient.
SubscribeOptions
Options for a StreamClient::subscribe firehose.
TaprootOutput
One of a transaction’s taproot outputs — a scan candidate. Carried in TweakEntry::taproot_outputs so a client can confirm a derived output key against the actual on-chain output without fetching the transaction.
TweakEntry
One transaction’s public silent-payment tweak, carried in a Event::BlockTweaks for Tier 1 client-side scanning.
WatchAddRejected
The payload of Event::WatchAddRejected: which add was refused, why, and the items it named. Only the item field for kind is set.
WatchHandle
A handle to send control messages on a bidirectional Watch stream.
WatchSetBuilder
The canonical watch-set declared by a watch_set_loader on every (re)connect. It is a thin recording facade over a fresh [WatchSetMirror]: each add_* / set_categories call records interest the way the matching ResilientWatch method would, so the populated set renders to exactly the same control messages a manual replay would. The wrapper adopts the builder’s set as the mirror once the loader returns, then re-registers it and re-anchors as usual.

Enums§

AutoClose
Auto-close policy for a transaction lifecycle watch: self-evict and emit TxidFinalized once the tx is buried this many confirmations deep.
CursorRejectReason
Why a mid-stream re-anchor was declined by the server (see Event::CursorRejected).
Event
A typed streaming event — the flat mirror of NodeEvent.body.
EvictReason
Why a transaction left the mempool by policy.
LagPolicy
What the resilience loop does with a Lagged notice.
ReloadError
Why a ResilientWatch::reload could not run.
RescanRejectReason
Why a bounded historical rescan was declined by the server (see Event::RescanRejected).
StatusKind
Which node-health condition an Event::Status describes.
StatusSeverity
How loud a health condition is.
StatusState
Level-triggered lifecycle of a health condition.
StreamError
An error from the streaming client.
WatchAddKind
Which kind of add a WatchAddRejected refers to.
WatchAddRejectReason
Why the server refused an incremental add (see Event::WatchAddRejected).
WatchSetRejectReason
Why an atomic watch-set replace was declined by the server (see Event::WatchSetRejected).

Constants§

MAX_SP_LABELS_PER_TARGET
Server-enforced cap on the number of distinct labels one scan-key target may carry (mirrors the node’s MAX_SP_LABELS_PER_TARGET). The server rejects an over-label target; enforcing the same bound client-side turns that into a deterministic error at the call site instead of a silent skip.
MAX_SP_TARGETS_PER_CONNECTION
Server-enforced cap on the number of scan-key targets per connection (mirrors the node’s MAX_SP_TARGETS_PER_CONNECTION). The server silently sheds an over-cap add; a stateful ResilientWatch enforces this bound before recording/sending so it never mirrors and replays a target the server dropped.

Traits§

CursorStore
Persists the durable resume Cursor across reconnects and process restarts. The resilience loop loads it on (re)connect and persists it commit-on-poll: a delivered event’s cursor is written only on the next next call, i.e. once the caller has come back for the following event (an implicit ack). The store therefore never advances past an event the caller has not yet received, so a crash mid-processing replays that event on resume — at-least-once, not at-most-once. (A consumer that needs exactly-once still dedups on its own side keyed by the (height, hash) it processes.)

Functions§

display_hex
Render a raw wire hash/txid (32 bytes, internal / consensus byte order, as every hash and txid on this API is carried) as a lowercase hex string in the reversed display order used by block explorers and Bitcoin Core JSON-RPC.
parse_txidbitcoin
Parse a raw wire txid into a typed bitcoin::Txid. Requires the bitcoin feature. Parse a raw wire txid (32 bytes, internal/consensus byte order) into a typed bitcoin::Txid. The returned value’s Display is the usual explorer / JSON-RPC order, and it hands straight to bitcoin-ecosystem APIs (bitcoincore-rpc, comparison against getrawtransaction, …).
prefix_ofbitcoin
Derive the (prefix, bits) registration tuple for a scriptPubKey: the top ceil(bits/8) bytes of its scripthash. bits is clamped to 1..=32 — the server buckets on the top 32 bits of the scripthash only, so a wider prefix is meaningless and would be silently dropped; WatchHandle::add_script_prefixes re-validates the same bound before the wire. Bits below bits are masked off. The wire carries whole bytes, so a 12-bit bucket ships 2 bytes while the server keys on the top 12 bits and masks the rest away (prefix_bucket_key, node/src/events/watch.rs). Sending those 4 extra bits unmasked hands the server 16× more of the scripthash than the declared bucket width — a free narrowing of the anonymity set the consumer asked for. Masking costs nothing: the bucket key is identical either way.
scripthash_ofbitcoin
sha256(scriptPubKey) — the 32-byte scripthash the server keys watches on (the prefix bucket is its top ceil(bits/8) bytes, big-endian).