Skip to content
Galaxoid-LabsPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

btcutils

Typed Odin bindings for a Bitcoin Core node, over both of its channels:

  • RPC — complete. Named, typed wrappers for the entire JSON-RPC surface of Bitcoin Core 30.x (149 methods: 93 node-level + 56 wallet), plus a dynamic escape hatch for anything you'd rather call by hand.
  • ZMQ — complete. A typed notification stream for all five topics (hashblock, hashtx, rawblock, rawtx, sequence), each event carrying its per-topic sequence counter for gap detection.

It is pure Bitcoin domain knowledge: all wire/transport/execution work (HTTP, JSON-RPC correlation, TLS, ZeroMQ/ZMTP) is reused from netclients, pulled in as a nested git submodule. btcutils writes no socket, TLS, framing, or HTTP code.

The intended pattern is two channels working together: RPC to ask, ZMQ to be told. ZMQ is a low-latency hint; authoritative state is always confirmed via RPC.

Requirements

  • Odin dev-2026-06 (uses the redesigned core:os).
  • OpenSSL 3.x (inherited from netclients; needed even for a plain-http:// local node because the engine links it):
    • macOS: brew install openssl@3
    • Linux: libssl-dev (apt install libssl-dev)
    • Windows: an OpenSSL install (import libs + DLLs on PATH)

Installing into your Odin project

btcutils has no package manager (Odin has none); use a git submodule. Because btcutils itself vendors netclients as a nested submodule, clone recursively.

# from your project root
git submodule add https://github.com/Galaxoid-Labs/btcutils btcutils
git submodule update --init --recursive      # pulls btcutils AND its nested netclients

If you (or a CI job) cloned without --recursive, backfill the nested submodule with the same git submodule update --init --recursive.

Then just import it — no collection flags needed

btcutils is self-contained: it imports its netclients engine by relative path, so whatever resolves the btcutils package automatically finds netclients beneath it. With btcutils vendored under your project, a plain relative import and a plain build are enough:

import btc "btcutils"   // relative to the importing file's directory
odin build .                 # no -collection flags

Odin resolves a relative import like btcutils from the directory of the file doing the import (not the project root), and a package's own relative imports resolve from its directory — which is why the nested netclients is found with no configuration.

Quick start

Connect

import btc "btcutils"

// Cookie auth (what most nodes use): reads <datadir>/<net>/.cookie automatically.
c := btc.btc_connect_cookie("/path/to/datadir", .Mainnet) or_return
defer btc.btc_destroy(&c)

// …or explicit rpcuser/rpcpassword:
// c := btc.btc_connect("user", "pass", .Mainnet) or_return

// …or a full URL you assembled yourself:
// c := btc.btc_connect_url("http://__cookie__:pass@127.0.0.1:8332/") or_return

Networks: .Mainnet (8332), .Testnet (18332), .Testnet4 (48332), .Signet (38332), .Regtest (18443). Host/port default to 127.0.0.1 and the network's default; override with the host/port args.

Typed RPC calls

count := btc.getblockcount(&c) or_return

hash := btc.getblockhash(&c, count) or_return
defer delete(hash)

info := btc.getblockchaininfo(&c) or_return
defer btc.blockchain_info_destroy(&info)
fmt.printfln("chain=%s height=%d ibd=%v", info.chain, info.blocks, info.initial_block_download)

blk := btc.getblock(&c, hash) or_return
defer btc.block_destroy(&blk)
fmt.printfln("tip has %d txs", blk.n_tx)

tx := btc.getrawtransaction_verbose2(&c, blk.tx[1]) or_return   // needs -txindex
defer btc.tx_destroy(&tx)
fmt.printfln("vin=%d vout=%d fee=%v", len(tx.vin), len(tx.vout), tx.fee)

Wallet RPCs

Wallet methods target /wallet/<name>; derive a wallet-scoped handle with btc_wallet. Management calls (createwallet/loadwallet/listwallets/…) use the base client.

names := btc.listwallets(&c) or_return          // node-level
defer { for n in names { delete(n) }; delete(names) }

w := btc.btc_wallet(&c, "mywallet") or_return    // /wallet/mywallet handle
defer btc.btc_destroy(&w)

bal := btc.getbalances(&w) or_return
fmt.printfln("trusted=%v pending=%v", bal.mine.trusted, bal.mine.untrusted_pending)

addr := btc.getnewaddress(&w) or_return
defer delete(addr)

The dynamic escape hatch

Anything not (or not-yet) typed is reachable by name, or via call / call_typed:

import "core:encoding/json"

// dynamic: json.Value in/out
v := btc.call(&c, "getmempoolinfo") or_return
defer json.destroy_value(v)

// typed: marshal params (a slice → positional array), unmarshal into T
h := btc.call_typed(&c, "getblockhash", []int{800_000}, string) or_return
defer delete(h)

ZMQ notifications

Enable in bitcoin.conf, e.g. zmqpubhashblock=tcp://127.0.0.1:28332, zmqpubrawtx=…, zmqpubsequence=….

n := btc.btc_zmq_dial("tcp://127.0.0.1:28332", {.Hash_Block, .Raw_Tx, .Sequence}) or_return
defer btc.btc_zmq_destroy(&n)

for {
    ev, err := btc.btc_zmq_recv(&n)
    if err != nil { break }                 // Net_Error.Connection_Closed at stream end
    defer btc.btc_zmq_event_destroy(&ev)

    switch e in ev {
    case btc.Hash_Block:
        hash := btc.zmq_hash_hex(e.hash); defer delete(hash)
        blk := btc.getblock(&c, hash) or_continue   // confirm via RPC
        defer btc.block_destroy(&blk)
    case btc.Raw_Tx:    /* e.bytes, e.seq */
    case btc.Sequence:  /* e.hash, e.label (C/D/A/R), e.mempool_seq, e.seq */
    case btc.Hash_Tx, btc.Raw_Block:
    }
}

An empty topic set ({}) subscribes to everything. Each event exposes .seq, the per-topic ZMQ counter — a gap means a dropped message; resync via RPC.

Concurrency: threaded and async

Calls are synchronous by default. For background fan-out, use a thread Pool (each call runs on a worker; await returns the decoded result):

p: btc.Pool
btc.btc_pool_start(&p, &c, 8)        // inits in place — never copy/move p after this
defer btc.btc_pool_shutdown(&p)

f1 := btc.btc_submit(&p, "getblockcount") or_return
f2 := btc.btc_submit(&p, "getblockhash", btc.params(800_000)) or_return
n  := btc.btc_await_typed(&p, f1, int) or_return
h  := btc.btc_await_typed(&p, f2, string) or_return; defer delete(h)

For event-loop / non-blocking use there's an async (core:nbio) surface — you own the loop:

import "core:nbio"

nbio.acquire_thread_event_loop(); defer nbio.release_thread_event_loop()
state: My_State
btc.btc_async_call(&c, "getblockcount", nil, on_count, &state) or_return
nbio.run_until(&state.done)
on_count :: proc(result: json.Value, err: btc.Error, user: rawptr) { /* result borrowed — copy now */ }

// async ZMQ notifier (typed events, same Zmq_Event union):
n := btc.btc_zmq_async_dial("tcp://127.0.0.1:28332", {.Hash_Block}, on_ev, on_err, &state) or_return
defer btc.btc_zmq_async_destroy(n)
nbio.run()
on_ev :: proc(ev: btc.Zmq_Event, user: rawptr) { /* ev borrowed — valid only during the call */ }

Async/threaded ride netclients' nbio + worker-pool drivers. Async RPC works over http:// and https:// (TLS verify is on by default; pass options with ca_file/insecure_skip_verify for HTTPS). Async ZMQ is tcp:// + NULL security (ZeroMQ's localhost model — not a TLS gap). Callback payloads (RPC result, async ZMQ event) are borrowed: valid only during the call; copy out what you keep.

Error handling — three tiers, kept distinct

Every call returns a single btc.Error union. The error model (and its variants) are re-exported from btcutils, so you never import netclients yourself:

  1. Transport — btc.Net_Error / btc.Tls_Error (node down, timeout, …).
  2. Malformed envelope — btc.Protocol_Error.
  3. A server fault over a healthy transport — btc.Rpc_Fault, carrying Bitcoin Core's {code, message}. This is not a transport error.
_, err := btc.getblock(&c, "deadbeef")
if code, ok := btc.fault_code(err); ok {
    switch code {
    case btc.RPC_INVALID_ADDRESS_OR_KEY: // -5: block not found
    case btc.RPC_INVALID_PARAMETER:      // -8
    }
}

// or match the variant directly:
if f, ok := err.(btc.Rpc_Fault); ok {
    fmt.eprintln(f.code, f.message)
    fe := err; btc.error_destroy(&fe)   // a captured fault owns heap memory
}

All Bitcoin Core RPC error codes are exported as btc.RPC_* constants (see errors.odin).

Memory / ownership

Typed results are unmarshalled into the call's allocator; their string/slice/map fields are heap-owned. Free them with the matching *_destroy (e.g. block_destroy, tx_destroy, wallet_info_destroy, peer_info_destroy), or unmarshal into an arena and reset that. Scalar results that allocate (a returned string) are freed with delete. ZMQ Raw_Block / Raw_Tx own their bytes — free with btc_zmq_event_destroy. The whole package is leak-tested.

Building / testing this repo

./build.sh check        # type-check (odin check . -no-entry-point)
./build.sh test         # offline, deterministic, node-free unit/vector tests
./build.sh example      # examples/blockcount against a local node (cookie auth)
./build.sh watch        # examples/watch — ZMQ + RPC confirm (needs zmqpub* configured)

# Live integration tests (gated): point at a running node.
#   BTCUTILS_LIVE=1      → RPC (sync + threaded + async) and read-only wallet
#   BTCUTILS_LIVE_ZMQ=1  → ZMQ notifier (sync + async); needs zmqpub* configured
# Datadir via BTCUTILS_DATADIR (default $HOME/Library/...); ZMQ via BTCUTILS_ZMQ_ENDPOINT.
BTCUTILS_LIVE=1 BTCUTILS_LIVE_ZMQ=1 BTCUTILS_DATADIR="$HOME/Library/Application Support/Bitcoin" \
  odin test . -define:ODIN_TEST_THREADS=1

No -collection flags anywhere — btcutils imports its netclients submodule by relative path, so it builds the same standalone or vendored in another project.

Layout

btcutils.odin        facade: Client, btc_connect*/btc_wallet, call/call_typed
errors.odin          Bitcoin RPC error codes + fault helpers (+ error-model re-exports)
types.odin           node-level result structs + destructors
blockchain.odin mining.odin network.odin rawtx.odin util.odin control.odin
wallet.odin wallet_types.odin
zmq.odin zmq_events.odin    typed ZMQ notifier + topic decoders
threaded.odin        Pool: btc_pool_start/btc_submit/btc_await(_typed)
async.odin           btc_async_call/notify + btc_zmq_async_dial (core:nbio)
params.odin          params(...) builder + internal call helpers
*_test.odin          offline vector tests; live_test.odin / live_zmq_test.odin /
                     live_concurrency_test.odin (gated)
examples/blockcount  examples/watch
.claude/skills/btcutils   usage skill (SKILL.md + references/)
netclients           git submodule (the engine)

License

See the repository.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages