You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
ndg/src/bitcoindrpc.zig

283 lines
10 KiB
Zig

//! a bitcoin core RPC client.
const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
const Atomic = std.atomic.Value;
const base64enc = std.base64.standard.Encoder;
const types = @import("types.zig");
pub const Client = struct {
allocator: std.mem.Allocator,
cookiepath: []const u8,
addr: []const u8 = "127.0.0.1",
port: u16 = 8332,
// each request gets a new ID with a value of reqid.fetchAdd(1, .monotonic)
reqid: Atomic(u64) = Atomic(u64).init(1),
pub const Method = enum {
getblockchaininfo,
getblockhash,
getmempoolinfo,
getnetworkinfo,
};
pub const RpcError = error{
// json-rpc 2.0
RpcInvalidRequest,
RpcMethodNotFound,
RpcInvalidParams,
RpcInternalError,
RpcParseError,
// general purpose errors
RpcMiscError,
RpcTypeError,
RpcInvalidAddressOrKey,
RpcOutOfMemory,
RpcInvalidParameter,
RpcDatabaseError,
RpcDeserializationError,
RpcVerifyError,
RpcVerifyRejected,
RpcVerifyAlreadyInChain,
RpcInWarmup,
RpcMethodDeprecated,
// p2p client errors
RpcClientNotConnected,
RpcClientInInitialDownload,
RpcClientNodeAlreadyAdded,
RpcClientNodeNotAdded,
RpcClientNodeNotConnected,
RpcClientInvalidIpOrSubnet,
RpcClientP2pDisabled,
RpcClientNodeCapacityReached,
// chain errors
RpcClientMempoolDisabled,
};
pub fn Result(comptime m: Method) type {
return types.Deinitable(ResultValue(m));
}
pub fn ResultValue(comptime m: Method) type {
return switch (m) {
.getblockchaininfo => BlockchainInfo,
.getblockhash => []const u8,
.getmempoolinfo => MempoolInfo,
.getnetworkinfo => NetworkInfo,
};
}
pub fn MethodArgs(comptime m: Method) type {
return switch (m) {
.getblockchaininfo, .getmempoolinfo, .getnetworkinfo => void,
.getblockhash => struct { height: u64 },
};
}
fn RpcRequest(comptime m: Method) type {
return struct {
jsonrpc: []const u8 = "1.0",
id: u64,
method: []const u8,
params: MethodArgs(m),
};
}
fn RpcResponse(comptime m: Method) type {
return struct {
id: u64,
result: ?ResultValue(m),
@"error": ?struct {
code: isize,
//message: ?[]const u8, // no use for it atm
},
};
}
/// makes an RPC call to the addr:port endpoint.
/// the returned value must be deinit'ed when done.
pub fn call(self: *Client, comptime method: Method, args: MethodArgs(method)) !Result(method) {
const addrport = try std.net.Address.resolveIp(self.addr, self.port);
const reqbytes = try self.formatreq(method, args);
defer self.allocator.free(reqbytes);
// connect and send the request
const stream = try std.net.tcpConnectToAddress(addrport);
defer stream.close();
const reader = stream.reader();
_ = try stream.writer().writeAll(reqbytes);
// read and parse the response
try skipResponseHeaders(reader, 4096);
const body = try reader.readAllAlloc(self.allocator, 1 << 20); // 1Mb should be enough for all response types
defer self.allocator.free(body);
return self.parseResponse(method, body);
}
/// reads all response headers, at most `limit` bytes, and returns the index
/// at which response body starts or error.EndOfStream.
/// single header length must be at most `limit` or 1024, whichever is smaller.
fn skipResponseHeaders(r: anytype, comptime limit: usize) !void {
var n: usize = 0;
var buf: [@min(1024, limit)]u8 = undefined;
while (true) {
const slice = try r.readUntilDelimiter(&buf, '\n');
n += slice.len + 1; // delimiter is not included in the slice
if (n > limit) {
return error.StreamTooLong;
}
if (slice.len == 0 or (slice.len == 1 and slice[0] == '\r')) {
return;
}
}
}
fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) {
var resp = try types.Deinitable(RpcResponse(m)).init(self.allocator);
errdefer resp.deinit();
resp.value = try std.json.parseFromSliceLeaky(RpcResponse(m), resp.arena.allocator(), b, .{
.ignore_unknown_fields = true,
.allocate = .alloc_always,
});
if (resp.value.@"error") |errfield| {
return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
}
if (resp.value.result == null) {
return error.NullResult;
}
return .{ .value = resp.value.result.?, .arena = resp.arena };
}
/// callers own returned value.
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
const req = RpcRequest(m){
.id = self.reqid.fetchAdd(1, .monotonic),
.method = @tagName(m),
.params = args,
};
var jreq = std.ArrayList(u8).init(self.allocator);
defer jreq.deinit();
try std.json.stringify(req, .{}, jreq.writer());
const auth = try self.getAuthBase64();
defer self.allocator.free(auth);
var bytes = std.ArrayList(u8).init(self.allocator); // return value as owned slice
const w = bytes.writer();
try w.writeAll("POST / HTTP/1.0\r\n");
//try w.writeAll("Host: 127.0.0.1\n", .{});
try w.writeAll("Connection: close\r\n");
try w.print("Authorization: Basic {s}\r\n", .{auth});
try w.writeAll("Accept: application/json-rpc\r\n");
try w.writeAll("Content-Type: application/json-rpc\r\n");
try w.print("Content-Length: {d}\r\n", .{jreq.items.len});
try w.writeAll("\r\n");
try w.writeAll(jreq.items);
return try bytes.toOwnedSlice();
}
fn getAuthBase64(self: Client) ![]const u8 {
const file = try std.fs.openFileAbsolute(self.cookiepath, .{ .mode = .read_only });
defer file.close();
const cookie = try file.readToEndAlloc(self.allocator, 1024);
defer self.allocator.free(cookie);
const auth = try self.allocator.alloc(u8, base64enc.calcSize(cookie.len));
return base64enc.encode(auth, cookie);
}
// taken from bitcoind source code.
// see https://github.com/bitcoin/bitcoin/blob/64440bb73/src/rpc/protocol.h#L23
fn rpcErrorFromCode(code: isize) ?RpcError {
return switch (code) {
// json-rpc 2.0
-32600 => error.RpcInvalidRequest,
-32601 => error.RpcMethodNotFound,
-32602 => error.RpcInvalidParams,
-32603 => error.RpcInternalError,
-32700 => error.RpcParseError,
// general purpose errors
-1 => error.RpcMiscError,
-3 => error.RpcTypeError,
-5 => error.RpcInvalidAddressOrKey,
-7 => error.RpcOutOfMemory,
-8 => error.RpcInvalidParameter,
-20 => error.RpcDatabaseError,
-22 => error.RpcDeserializationError,
-25 => error.RpcVerifyError,
-26 => error.RpcVerifyRejected,
-27 => error.RpcVerifyAlreadyInChain,
-28 => error.RpcInWarmup,
-32 => error.RpcMethodDeprecated,
// p2p client errors
-9 => error.RpcClientNotConnected,
-10 => error.RpcClientInInitialDownload,
-23 => error.RpcClientNodeAlreadyAdded,
-24 => error.RpcClientNodeNotAdded,
-29 => error.RpcClientNodeNotConnected,
-30 => error.RpcClientInvalidIpOrSubnet,
-31 => error.RpcClientP2pDisabled,
-34 => error.RpcClientNodeCapacityReached,
// chain errors
-33 => error.RpcClientMempoolDisabled,
else => null,
};
}
};
pub const BlockchainInfo = struct {
chain: []const u8,
blocks: u64,
headers: u64,
bestblockhash: []const u8,
difficulty: f64,
time: u64, // block time, unix epoch
mediantime: u64, // median block time, unix epoch
verificationprogress: f32, // estimate in [0..1]
initialblockdownload: bool,
size_on_disk: u64,
pruned: bool,
//pruneheight: ?u64, // present if pruning is enabled
//automatic_prunning: ?bool, // present if pruning is enabled
//prune_target_size: ?u64, // present if automatic is enabled
warnings: []const u8,
};
pub const MempoolInfo = struct {
loaded: bool, // whether the mempool is fully loaded
size: usize, // tx count
bytes: u64, // sum of all virtual transaction sizes as per BIP-141 (discounted witness data)
usage: u64, // total memory usage
total_fee: f32, // total fees in BTC ignoring modified fees through prioritisetransaction
maxmempool: u64, // memory usage cap, in bytes
mempoolminfee: f32, // min fee rate in BTC/kvB for tx to be accepted
minrelaytxfee: f32, // current min relay fee rate
incrementalrelayfee: f32, // min fee rate increment for replacement, in BTC/kvB
unbroadcastcount: u64, // number of transactions that haven't passed initial broadcast yet
fullrbf: bool, // whether the mempool accepts RBF without replaceability signaling inspection
};
pub const NetworkInfo = struct {
version: u32,
subversion: []const u8,
protocolversion: u32,
connections: u16, // in + out
connections_in: u16,
connections_out: u16,
networkactive: bool,
networks: []struct {
name: []const u8, // ipv4, ipv6, onion, i2p, cjdns
limited: bool, // whether this network is limited with -onlynet flag
reachable: bool,
},
relayfee: f32, // min rate, in BTC/kvB
incrementalfee: f32, // min rate increment for RBF in BTC/vkB
localaddresses: []struct {
address: []const u8,
port: u16,
score: i16,
},
warnings: []const u8,
};