Skip to content

clients/go: core client, typed events, and the first E2E layer - #513

Open
bkeroack wants to merge 1 commit into
feat/go-sdk-bootstrapfrom
feat/go-sdk-core
Open

clients/go: core client, typed events, and the first E2E layer#513
bkeroack wants to merge 1 commit into
feat/go-sdk-bootstrapfrom
feat/go-sdk-core

Conversation

@bkeroack

@bkeroack bkeroack commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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), and WithGRPCDialOption as the escape hatch. Requesting TLS against an explicit http:// target is refused rather than silently downgraded.
  • Subscribe*Stream with Recv (typed events, io.EOF on clean close) and Cursor (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.

Two deliberate divergences from the Rust client

Both documented at the call site:

  1. Optional wire values are pointers (*uint64, *uint32) so "the node did not retain this" stays distinct from a genuine zero.
  2. 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 gate 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 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 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.

The CI Tests job now runs the Go E2E suite against the same target/debug/satd the 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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant