A small, self-contained Rust tool that generates Rust bindings for the Bitcoin Core JSON-RPC API from Bitcoin Core's own OpenRPC export.
This repository's goal is to be consumed by the
corepc-client async production client as a
source of generated code that is committed to the consumer's tree. The tool runs only when
a maintainer regenerates bindings — the consumer crate never shells out to it at build time.
For each Bitcoin Core version N that has a spec under specs/, the tool writes four files
under output/v{N}/:
output/v{N}/
├── mod.rs — module entry point + re-exports
├── types.rs — return types (one struct per RPC response shape)
├── options.rs — *Options structs for RPCs with optional positional arguments
└── methods.rs — `impl<'a> Raw<'a> { ... }` blocks dispatching via Client::call_raw
Those four files are then copied into
corepc-client/src/client_async/codegen/v{N}/. They compile against the consumer's
Client::call_raw plumbing and require nothing else from this repo at runtime.
To make diffs unreviewable this splits by concern lets a reviewer auditing a new Core version skim each surface independently:
types.rsanswers "did the response shape change?"options.rsanswers "did optional parameters change?"methods.rsanswers "did dispatch shape change?"
Most Core releases touch one of those without the others.
Bitcoin Core's RPC takes optional arguments positionally, with null as the
"use default" sentinel. The tool emits two methods per RPC that has any optional parameters:
// Required-only — uses Core's defaults for everything optional.
client.raw().send_raw_transaction(hex).await?;
// With-options — every optional field is `Option<T>`; `None` serialises as JSON `null`,
// which Core treats as "use the documented default".
client.raw().send_raw_transaction_with(
hex,
SendRawTransactionOptions { max_fee_rate: Some(0.5), ..Default::default() },
).await?;Directly from this directory:
just codegen 30
just codegen-all
just test
just lint
just clean- Apply Bitcoin Core's spec-export patch (a
bitcoin-cli getopenrpcextension) to a checkout matching the version you want, build it, and copy the resulting JSON intospecs/v{major}_{minor}_{patch}_openrpc.json. - Regenerate from the corepc workspace root:
just codegen {major}. - Add the version feature in
corepc-client/Cargo.toml(e.g."31_0" = []) and gate the new module inclient_async/codegen/mod.rsandclient_async/model/mod.rs.
Hand-written model wrappers do not belong in this repo — they live in
corepc-client/src/client_async/model/v{N}.rs. The generated raw surface gives you
client.raw().some_rpc(); a model wrapper is a 5-line function on Client that delegates to
that and runs into_model().
Every spec change requires a maintainer to run just codegen and commit the
diff.
The generator carries a curated word list (METHOD_WORDS, FIELD_WORDS in src/names.rs)
used by a longest-match peeling scan to convert all-lowercase compounds. Two design rules drive
the contents:
- Plurals are listed only when they cannot be reconstructed by appending
s. Words ending in a consonant +s(blocks,txs,times) are omitted because keeping them shadows real word boundaries —blocks+tatswould otherwise win over the correctblock+statssplit ingetblockstats. - Single-word forms preferred over compound forms.
txoutis split intotx+outso we getGetTxOut, matchingcorepc-types. We pay a one-off cost for any compound that cannot decompose cleanly (prevoutis its own entry).
When a new Core release adds a noun the splitter doesn't know about, the output will look
"off" — add the noun to METHOD_WORDS / FIELD_WORDS and regenerate.
Bitcoin Core's OpenRPC export uses type: number for many fields that are semantically
integers (block height, verbosity level, conf targets). INTEGER_PARAM_NAMES in codegen.rs
maintains a list of parameter names that are integer-only across the entire RPC surface;
matches use i64. Anything else stays f64.
A future improvement is to patch Bitcoin Core's OpenRPC generator to emit type: integer
directly, at which point this list collapses to [].
Bitcoin Core's docstrings contain bare URLs, bracketed parameter references ([minconf]),
and angle-bracketed placeholders (<wallet name>). We escape the angle brackets so rustdoc
renders them literally and silence rustdoc's bare_urls and broken_intra_doc_links lints
at the generated module boundary, since those warnings are properties of the upstream prose
rather than mistakes in our codegen.
A handful of method names produce PascalCase types that collide with std prelude items —
notably send → Send (which would shadow the Send trait in the methods file). The
RESERVED_TYPE_NAMES table in codegen.rs rewrites these to non-colliding idents
(SendResult) for both the emitted struct and the method's Result<...> return-type
expression.
cargo test.
├── README.md
├── Cargo.toml ← self-contained crate manifest (empty `[workspace]` table keeps it
│ out of the corepc Cargo workspace)
├── justfile ← codegen / codegen-all / test / lint / clean
├── src/
│ ├── main.rs ← CLI: `btc-codegen <version|all>`
│ ├── lib.rs ← public `generate(spec, out_dir, version)` entry point
│ ├── names.rs ← word lists + identifier conversion (PascalCase / snake_case)
│ ├── spec.rs ← typed slice of OpenRPC JSON we need to read
│ └── codegen.rs ← schema → Rust translation; emits the four output files
├── specs/ ← committed OpenRPC specs, one per Bitcoin Core release
│ ├── v29_3_0_openrpc.json
│ └── v30_2_0_openrpc.json
└── output/ ← generated artefacts (git-ignored; regenerable)
└── v30/
├── mod.rs
├── types.rs
├── options.rs
└── methods.rs