IntersectMBO / IntersectMBO/cardano-api

cardano-rpc: split a lightweight client package out of the server

Open
#1,339 0 comments 0 reactions 1 assignee Claimed by @carbolymer View on GitHub
refactor
Dominant language
Haskell
Stars
40
Forks
30
Avg merge
2d 5h
Merged PRs (30d)
30

Description

## Problem

cardano-rpc now ships standalone quickstarts for Go, Rust and TypeScript under `cardano-rpc/quickstart/{go,rust,typescript}/`. Each is a small, self-contained project (`go.mod`, `Cargo.toml`, `package.json`) that depends only on generated gRPC bindings for its ecosystem: the Go example uses the published `github.com/utxorpc/go-codegen` module (tag `v0.19.2`), the Rust example uses the published `utxorpc-spec` crate, and the TypeScript example uses MeshJS plus a small local v1beta provider. None of the three needs proto files, protoc, or anything beyond its own language's normal package manager and toolchain, and each gets its own minimal `nix-shell`/`shell.nix` with just that language's compiler.

There is no equivalent Haskell quickstart of that shape, and a work-in-progress attempt to add one makes the reason concrete. Its example code (`app/Main.hs`) touches only `Cardano.Rpc.Client` and the generated `Cardano.Rpc.Proto.Api.UtxoRpc.{Query,Sync}` modules: it opens a connection, calls `ReadTip` and `ReadParams`, and reads a handful of fields off the response with proto-lens optics. It never mentions a `cardano-api` or `cardano-ledger` type. Despite that, its `.cabal` file has to depend on the whole `cardano-rpc` package (currently `^>=11.3`), because that is the only package that exposes `Cardano.Rpc.Client`. `cardano-rpc` in turn depends on the full `cardano-api`/`cardano-ledger`/`ouroboros-consensus` closure (roughly an hour of compilation) and on the project's Cardano-specific C library forks (the libsodium VRF fork, secp256k1, blst). As a result, the quickstart's own README has to tell readers to enter the whole repository's own development shell rather than a small per-language one, and the flake wires its dev shell to the repository's default shell rather than a lightweight one, because a plain nixpkgs shell cannot supply those C libraries. Every other language's quickstart needs only that language's toolchain; the Haskell one needs the entire monorepo's build environment for around thirty lines of wire-protocol code.

This is a symptom of a packaging problem, not an accident of the quickstart. `cardano-rpc`'s own `Cardano.Rpc.Client` module already reaches, transitively, into `cardano-api` and `cardano-ledger` today: it imports `Cardano.Rpc.Server.Internal.Orphans` for a handful of orphan `Inject` instances, and that module imports `Cardano.Api.Era`, `Cardano.Api.Error`, `Cardano.Api.Ledger`, `Cardano.Api.Pretty`, `Cardano.Api.Serialise.Raw` and `Cardano.Api.Tx`. So any Haskell consumer who wants nothing more than to call a couple of RPCs against generated proto types is already forced to build the same dependency closure as the full node server.

## Proposed split

We propose splitting `cardano-rpc` into two packages: a new, lightweight `cardano-rpc-client` containing the generated proto/gRPC bindings and the client-facing modules, and the existing `cardano-rpc` package, retained for the server implementation, depending on `cardano-rpc-client`.

Based on a read of the current module layout, the split falls out fairly cleanly:

**Moves to `cardano-rpc-client`** (no change needed beyond relocation): the whole of the current `library gen` sublibrary (`Proto.Cardano.Rpc.Node(_Fields)`, `Proto.Grpc.Reflection.V1(alpha).Reflection(_Fields)`, `Proto.Utxorpc.V1beta.{Cardano,Query,Submit,Sync}.*(_Fields)`), which depends only on `proto-lens`/`proto-lens-protobuf-types`/`proto-lens-runtime` and has no Cardano-specific dependency at all; and the six thin wrapper modules `Cardano.Rpc.Proto.Api.Node`, `Cardano.Rpc.Proto.Api.Reflection.V1`, `Cardano.Rpc.Proto.Api.Reflection.V1alpha`, `Cardano.Rpc.Proto.Api.UtxoRpc.Query`, `Cardano.Rpc.Proto.Api.UtxoRpc.Submit` and `Cardano.Rpc.Proto.Api.UtxoRpc.Sync`, which only import grapesy's `Network.GRPC.Common*` and the generated modules above.

**Stays in `cardano-rpc`** (the server): `Cardano.Rpc.Server`, `Cardano.Rpc.Server.Config`, every `Cardano.Rpc.Server.Internal.*` module (`Env`, `Error`, `Monad`, `Node`, `Reflection`, `Reflection.DescriptorTable`, `TimedCache`, `Tracing`, and the whole `UtxoRpc.{Eval,Predicate,Query,Submit,Sync,Type,Type.*}` tree), and `Cardano.Rpc.Server.NodeKernelAccess` with its internal type module. These all convert between ledger/node-kernel state and wire types, or wire up the node-kernel access and the gRPC method table, and there is no reason for a pure client to carry any of it. `Cardano.Rpc.Server.Config` in particular is purely about how the server is configured (listen endpoint, node socket path, TLS files) and has no bearing on client-side connection setup, so it stays put unchanged.

**Needs to split**: `Cardano.Rpc.Client` itself, and the `Cardano.Rpc.Server.Internal.Orphans` module it currently pulls in for instances. `Client.hs` moves to the new package, but its `import Cardano.Rpc.Server.Internal.Orphans ()` has to be replaced, because that module mixes instances that are genuinely independent of `cardano-api`/`cardano-ledger` with ones that are not. The independent ones are `instance Message a => Default (Proto a)`, `instance IsString e => MonadFail (Either e)`, `instance Inject Rational (Proto U5c.RationalNumber)` (pure `Data.Ratio` arithmetic that merely happens to be co-located in that module) and `instance Inject Int64 (Proto U5c.BigInt)`; these can move into the client package as they stand. `instance Inject Integer (Proto U5c.BigInt)` is a near-miss: its big-number branch calls `serialiseToRawBytes :: Natural -> ByteString`, which happens to be a `cardano-api` instance (`Cardano.Api.Serialise.Raw`), even though the big-endian byte encoding it performs is a few lines of generic logic with nothing Cardano-specific about it; reimplementing those few lines locally in the client package removes the dependency. The remaining instances, `Inject (Proto U5c.ExUnits) L.ExUnits`, `Inject L.ExUnits (Proto U5c.ExUnits)`, `Inject L.ProtVer (Proto U5c.ProtocolVersion)`, `Inject TxIn (Proto U5c.TxoRef)`, `Inject L.Coin (Proto U5c.BigInt)` and `Error StringException`, genuinely need ledger or `cardano-api` types and stay in `cardano-rpc`'s orphans module, still reachable by anything that depends on both packages.

One consequence of this boundary is worth noting for `cardano-testnet`. Its own integration tests (`cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/*.hs`) are real client-side consumers of `Cardano.Rpc.Client`, but they also reach directly into `cardano-rpc`'s server-internal conversion and predicate-building helpers (`Server.Internal.UtxoRpc.Type`, `Server.Internal.UtxoRpc.Predicate`) to build request predicates from ledger `Address`/`TxIn` values and to decode BigInt/Rational/UTxO/PParams responses back into ledger types for assertions. That is fine under this split: `cardano-testnet` is an integration test suite that already carries the full ledger closure for unrelated reasons (constructing and submitting transactions), so it can simply depend on both `cardano-rpc-client` and `cardano-rpc` rather than only the former. The split is aimed at external, non-node consumers who want only the wire protocol, not at the project's own test suites.

We considered exposing the client as a cabal public sublibrary (for example `cardano-rpc:client`) inside the existing package instead of a separate package, but rejected it: CHaP and haskell.nix key several of their mechanisms off whole-package names and versions, so a sublibrary would not give external consumers an independently versioned target with its own, genuinely minimal dependency closure the way a separate package does.

## Migration sketch

The rough shape of the work: create a new `cardano-rpc-client` package alongside `cardano-rpc` (its own `.cabal` file, `library gen` sublibrary and `src` tree); move the `gen/` generated modules and the six `Cardano.Rpc.Proto.Api.*` wrapper modules into it unchanged; move `Cardano.Rpc.Client` into it along with the ledger-free half of `Server.Internal.Orphans` (reimplementing the small `Natural` big-endian encoding it currently borrows from `cardano-api`); and have `cardano-rpc` depend on `cardano-rpc-client` in place of its former `gen` sublibrary and client modules. The `buf.gen.yaml`/`proto/` sources and the `buf generate` regeneration step described in `cardano-rpc/README.md` would need to move (or be shared) with the new package, since they are what produces the `gen/` tree.

For CHaP, `cardano-rpc-client` would need to be released first, at its own initial version, before `cardano-rpc` is bumped to depend on that released version and released in turn. During the transition it would be worth keeping deprecated re-export shims in `cardano-rpc` for the modules that moved, so that existing importers are not broken by the split before they have updated their dependencies. Once the new package is released, `cardano-node`'s `cardano-rpc ^>=11.3` bound (the only version-pinned dependency of this kind in cardano-node) needs updating for the new `cardano-rpc` version, and `cardano-testnet.cabal` needs an explicit `cardano-rpc-client` build-depends added, alongside the `cardano-rpc` dependency it keeps for the server-internal conversion helpers its tests already use.

## Benefits

A genuinely lightweight `cardano-rpc-client` gives the Haskell quickstart the same shape as the Go, Rust and TypeScript ones: install a small package, write a handful of lines against generated proto types, and run it in an ordinary per-language shell, with no Cardano C libraries and none of the `cardano-api`/`cardano-ledger`/`ouroboros-consensus` build time. More generally, it makes Haskell a cheap option for anyone who wants to speak the UTxO RPC protocol, whether against cardano-node or another UTxO RPC server such as Dolos, in the same way the existing `go-codegen`, `utxorpc-spec` and MeshJS bindings already do for their ecosystems.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.