feat(slv-plugins/rpc-gateway): Phase 4c — transactionSubscribe via Yellowstone gRPC#384
Merged
Merged
Conversation
…llowstone gRPC
Final piece of the WebSocket port. Adds the extended
`transactionSubscribe` / `transactionUnsubscribe` methods,
backed by a per-call Yellowstone-gRPC subscribe stream.
## What this PR ships
### `ws/yellowstone_bridge.rs`
`YellowstoneBridge` opens a fresh outbound gRPC `Subscribe` call
per `transactionSubscribe` invocation, builds the
`SubscribeRequest` from the client's filter + opts (commitment,
accountInclude / accountExclude / accountRequired etc.), and
forwards each `SubscribeUpdate::Transaction` event into a
caller-supplied closure as a JSON `transactionNotification` frame.
Filter / opts use neutral names (`TxSubscribeFilter`,
`TxSubscribeOpts`) — matches the rename in
`b895f1e chore(rpc-gateway): neutralize vendor-specific phrasing
in public code`.
### `ws/mod.rs` — dispatcher wired up
`transactionSubscribe` now spawns a forwarder task and replies
with a local sub-id (≥ `LOCAL_SUB_ID_BASE`). The task drains
the gRPC stream and pushes notifications into the per-client
outbound mpsc. `transactionUnsubscribe` looks up the matching
`JoinHandle` in `local_subs` and `abort()`s it, which drops the
gRPC stream cleanly. Connection close shuts down all local
subs the same way (= existing `ConnectionState::shutdown`).
### `dispatch.rs` — `Gateway` carries the gRPC endpoint
`Gateway::yellowstone_endpoint` (= host:port, scheme coerced by
the bridge) so the WS handler can clone it per call.
`GatewayBuilder::yellowstone_endpoint` carries it from env.
### `main.rs` — new env
YELLOWSTONE_GRPC default `localhost:10000` — matches the
Deno gateway
## Trade-off (= clearly documented in code)
The Rust port currently emits a smaller `result` shape than the
Deno gateway in `transactionDetails: "full"` mode:
```
{ signature, slot, blockTime: null, transaction: { signatures },
isVote, err: { encoded: <base58 of proto err bytes> } }
```
Reason: the Rust proto crate doesn't ship Serialize for
`TransactionStatusMeta` and the npm Deno package does. Clients
that want the full parsed transaction body should call
`getTransaction(signature)` after the notification. Tracked for
a follow-up adding the `UiTransactionStatusMeta` conversion.
`"none"` and `"signatures"` modes are wire-compatible.
## Verified
- `cargo check -p slv-rpc-gateway` clean
- `cargo test -p slv-rpc-gateway` — **42 passed, 0 failed**
(41 prior + revised + 4 new yellowstone-bridge tests):
- `endpoint_coerces_bare_hostport` / `passes_https_through` /
`strips_grpc_scheme` — URL normalisation
- `build_subscribe_request_uses_default_commitment_confirmed` —
default commitment + filter map key shape
- `build_subscribe_request_carries_filter_lists` — filter +
commitment override
- `transform_to_notification_none_detail_returns_minimal` —
detail=none projection
- Revised `ws_accepts_transaction_subscribe_and_returns_local_sub_id` —
full end-to-end through the WS layer; gRPC connect fails in
CI (no upstream) but the immediate ACK with a local sub-id ≥
`LOCAL_SUB_ID_BASE` proves the WS path is wired
## Roadmap completion
Phase 4c lands the last surface the Deno gateway implements:
| Phase | Methods | Status |
|---|---|---|
| 0 | dispatch + /health | merged |
| 1 | 5 `jet*` analytics | merged |
| 2a | `getTransactionsForAddress` | merged |
| 2b | `getTransfersByAddress` | merged |
| 3 | upstream pass-through proxy | merged |
| 4a | WebSocket scaffold + standard pubsub | merged |
| 4b | `slotSubscribe` multi-source fan-in | merged |
| **4c** | **`transactionSubscribe`** | **this PR** |
The Rust gateway now matches the full feature surface of the
Deno gateway at `api/rpc-gateway/`. Per-method byte-for-byte
diff verification + load-balancer cutover can proceed
incrementally; the Deno gateway stays in production until each
method is verified and switched.
## Follow-ups (= not blocking the cutover)
- Add `UiTransactionStatusMeta` projection for `transactionSubscribe`'s
`full` mode so the JSON shape matches the Deno gateway exactly
- Revive `SLOT_BRIDGE_GRPC` (gRPC slot source) — same `GeyserGrpcClient`
the bridge already uses
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Final piece of the WebSocket port. Adds the extended
transactionSubscribe/transactionUnsubscribemethods backed by a per-call Yellowstone-gRPC subscribe stream.What this PR ships
ws/yellowstone_bridge.rsYellowstoneBridgeopens a fresh outbound gRPCSubscribecall pertransactionSubscribeinvocation, builds theSubscribeRequestfrom the client's filter + opts, and forwards eachSubscribeUpdate::Transactionevent into a caller-supplied closure as a JSONtransactionNotificationframe.Filter / opts use neutral names (
TxSubscribeFilter,TxSubscribeOpts) — matches the rename inb895f1e.ws/mod.rstransactionSubscribespawns a forwarder task and replies with a local sub-id (≥LOCAL_SUB_ID_BASE). The task drains the gRPC stream and pushes notifications into the per-client outbound mpsc.transactionUnsubscribelooks up the matchingJoinHandleinlocal_subsandabort()s it.dispatch.rs+main.rsGateway::yellowstone_endpointcarries the gRPC host. New env:YELLOWSTONE_GRPC(defaultlocalhost:10000).Trade-off (documented in code)
The Rust port currently emits a smaller
resultshape than the Deno gateway intransactionDetails: "full"mode:Reason: the Rust proto crate doesn't ship Serialize for
TransactionStatusMetaand the npm Deno package does. Clients that want the full parsed transaction body should callgetTransaction(signature)after the notification. Tracked for a follow-up adding theUiTransactionStatusMetaconversion."none"and"signatures"modes are wire-compatible.Verified
cargo check -p slv-rpc-gatewaycleancargo test -p slv-rpc-gateway— 42 passed, 0 failed (37 prior + 4 new + 1 revised)Roadmap completion
jet*analyticsgetTransactionsForAddressgetTransfersByAddressslotSubscribemulti-source fan-intransactionSubscribeThe Rust gateway now matches the full feature surface of the Deno gateway at
api/rpc-gateway/. Per-method byte-for-byte diff verification + load-balancer cutover can proceed incrementally; the Deno gateway stays in production until each method is verified and switched.Follow-ups (not blocking cutover)
UiTransactionStatusMetaprojection sotransactionSubscribe'sfullmode matches Deno exactlySLOT_BRIDGE_GRPC(gRPC slot source) using the sameGeyserGrpcClient🤖 Generated with Claude Code