clients/go: core client, typed events, and the first E2E layer - #513
Open
bkeroack wants to merge 1 commit into
Open
clients/go: core client, typed events, and the first E2E layer#513bkeroack wants to merge 1 commit into
bkeroack wants to merge 1 commit into
Conversation
The SDK's foundation: connect, subscribe, and a typed event model, plus the two test layers that keep them honest. Public surface: - `Dial(ctx, target, ...Option)` with functional options for bearer auth, TLS/mTLS/CA-pinning/SNI, keepalive (on by default at the server's 30s/20s), and a `WithGRPCDialOption` escape hatch for anything not wrapped. - `Subscribe` returning a `*Stream` whose `Recv` yields typed events and whose `Cursor` captures the durable resume position. - All 30 typed events behind a sealed `Event` interface, consumed by type switch — the Go analog of the Rust enum — plus the seven wire enums. - `*Error` with class sentinels for `errors.Is`, the gRPC status preserved for `errors.As`, and `Retryable` classification carried over from the Rust SDK. - `DisplayHex` / `ParseTxid` / `TxidFromDisplayHex`, documented above the fold: the wire is internal byte order, JSON-RPC and explorers are reversed, and that mismatch is the most common integration bug against this API. Two deliberate divergences from the Rust client, both documented at the call site. Optional wire values are pointers (`*uint64`, `*uint32`) so "the node did not retain this" stays distinct from a genuine zero. And open enums are plain int32 types that carry an unrecognized value through unchanged, rather than a separate Unknown variant — which keeps "unset" and "set to something newer than this build" distinct without a wrapper. `StatusSeverity` keeps the Rust severity RANK (an unrecognized level outranks Critical, an unset one ranks below Info) via `AtLeast`/`Compare` rather than raw numeric order. Testing, both layers gating from here on: - Decoder exhaustiveness: a protobuf-reflection walk of the `NodeEvent.body` oneof — and of the five nested oneofs it hides — asserts every arm maps to a typed event, so a proto addition that lands without Go support fails on the PR that adds it. A companion walk pins every enum constant and Known() range against the descriptor. This is the Go stand-in for Rust's exhaustive match. - Go E2E against a live node (`clients/go/e2e`, build tag `e2e`, its own module): block connect, the full mempool lifecycle with real fee/vsize, category-filter exclusion, cursor capture and replay-on-resume, heartbeat cadence, a first-class reorg, and a dead node surfacing a retryable error. The harness builds and signs its spends through the node's own createrawtransaction/signrawtransactionwithkey RPCs, so the suite needs no Bitcoin library in Go — the SDK does not force one on consumers and its tests should not quietly depend on one either. Every new assertion was perturbed once and observed to fail before landing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJqZbJtTUo7K7G9wJcrvHv
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR 2 of the Go SDK stack. Stacked on #512 — merge that first.
What this lands
The SDK's foundation: connect, subscribe, a typed event model, and the two test layers that keep them honest.
Dial(ctx, target, ...Option)— functional options for bearer auth, TLS / mTLS / CA-pinning / SNI, keepalive (on by default at the server's 30s/20s), andWithGRPCDialOptionas the escape hatch. Requesting TLS against an explicithttp://target is refused rather than silently downgraded.Subscribe→*StreamwithRecv(typed events,io.EOFon clean close) andCursor(the durable resume position).Eventinterface, consumed by type switch — the Go analog of the Rust enum — plus the seven wire enums.*Errorwith class sentinels forerrors.Is, the gRPC status preserved forerrors.As, andRetryableclassification carried over from the Rust SDK.DisplayHex/ParseTxid/TxidFromDisplayHex, documented above the fold.Two deliberate divergences from the Rust client
Both documented at the call site:
*uint64,*uint32) so "the node did not retain this" stays distinct from a genuine zero.int32types that carry an unrecognized value through unchanged, rather than a separateUnknownvariant — which keeps "unset" and "set to something newer than this build" distinct without a wrapper.StatusSeveritykeeps the Rust severity rank (an unrecognized level outranksCritical; an unset one ranks belowInfo) viaAtLeast/Comparerather than raw numeric order.Testing — both layers gate from here on
Decoder exhaustiveness. A protobuf-reflection walk of the
NodeEvent.bodyoneof — and of the five nested oneofs it hides — asserts every arm maps to a typed event, so a proto addition that lands without Go support fails on the PR that adds it. A companion walk pins every enum constant andKnown()range against the descriptor. This is the Go stand-in for Rust's exhaustivematch.Go E2E against a live node (
clients/go/e2e, build tage2e, its own module so test-only deps never reach the published graph): block connect, the full mempool lifecycle with real fee/vsize, category-filter exclusion, cursor capture and replay-on-resume, heartbeat cadence, a first-class reorg, and a dead node surfacing a retryable error. The harness builds and signs its spends through the node's owncreaterawtransaction/signrawtransactionwithkeyRPCs, so the suite needs no Bitcoin library in Go — the SDK does not force one on consumers and its tests should not quietly depend on one either.The CI Tests job now runs the Go E2E suite against the same
target/debug/satdthe Rust E2E step just built, serialized for the same port-contention reason.Negative verification: every new assertion was perturbed once and observed to fail before landing — the exhaustiveness walk (an arm returning unknown), the optional-vs-zero mapping, the category filter, the cursor resume anchor, the heartbeat cadence, and the reorg tip.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GJqZbJtTUo7K7G9wJcrvHv