bitcoin rpc doesn't seem to work; resort to cmdline
parent
746b179478
commit
ff88304278
@ -0,0 +1,275 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Atomic = std.atomic.Atomic;
|
||||||
|
const base64enc = std.base64.standard.Encoder;
|
||||||
|
|
||||||
|
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 MethodTag = enum {
|
||||||
|
getblockhash,
|
||||||
|
getblockchaininfo,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// makes an RPC call to the addr:port endpoint.
|
||||||
|
/// the returned value always has a .result field of type ResultType(method);
|
||||||
|
/// all other fields are for internal use.
|
||||||
|
///
|
||||||
|
/// callers must free resources allocated for the retuned value using its `free` function.
|
||||||
|
pub fn call(self: *Client, comptime method: MethodTag, args: MethodArgs(method)) !CallRetType(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);
|
||||||
|
errdefer stream.close();
|
||||||
|
const reader = stream.reader();
|
||||||
|
_ = try stream.writer().writeAll(reqbytes);
|
||||||
|
|
||||||
|
// read response
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
var resbytes = std.ArrayList(u8).init(self.allocator);
|
||||||
|
defer resbytes.deinit();
|
||||||
|
while (true) {
|
||||||
|
// TODO: use LimitedReader
|
||||||
|
const n = try reader.read(&buf);
|
||||||
|
if (n == 0) {
|
||||||
|
break; // EOS
|
||||||
|
}
|
||||||
|
try resbytes.appendSlice(buf[0..n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for end of headers in the response
|
||||||
|
var last_byte: u8 = 0;
|
||||||
|
var idx: usize = 0;
|
||||||
|
for (resbytes.items) |v, i| {
|
||||||
|
if (v == '\r') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (v == '\n' and last_byte == '\n') {
|
||||||
|
idx = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last_byte = v;
|
||||||
|
}
|
||||||
|
if (idx == 0 or idx >= resbytes.items.len) {
|
||||||
|
return error.NoBodyInResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the response body and return its .result field or an error.
|
||||||
|
var arena_state = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
errdefer arena_state.deinit();
|
||||||
|
const arena = arena_state.allocator();
|
||||||
|
var jstream = std.json.TokenStream.init(resbytes.items[idx..]);
|
||||||
|
const jopt = std.json.ParseOptions{ .allocator = arena, .ignore_unknown_fields = true };
|
||||||
|
const Typ = RpcRespType(method);
|
||||||
|
@setEvalBranchQuota(2000); // std/json.zig:1520:24: error: evaluation exceeded 1000 backwards branches
|
||||||
|
const resp = try std.json.parse(Typ, &jstream, jopt);
|
||||||
|
if (resp.@"error") |errfield| {
|
||||||
|
return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
|
||||||
|
}
|
||||||
|
if (resp.result == null) {
|
||||||
|
return error.NullResult;
|
||||||
|
}
|
||||||
|
return .{ .result = &resp.result.?, .arena = arena_state };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn CallRetType(comptime method: MethodTag) type {
|
||||||
|
return struct {
|
||||||
|
result: *const ResultType(method),
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
|
||||||
|
pub fn free(self: @This()) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn MethodArgs(comptime m: MethodTag) type {
|
||||||
|
return switch (m) {
|
||||||
|
.getblockchaininfo, .getnetworkinfo => void,
|
||||||
|
.getblockhash => struct { height: u64 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn ResultType(comptime m: MethodTag) type {
|
||||||
|
return switch (m) {
|
||||||
|
.getblockchaininfo => BlockchainInfo,
|
||||||
|
.getnetworkinfo => NetworkInfo,
|
||||||
|
.getblockhash => []const u8,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn RpcReqType(comptime m: MethodTag) type {
|
||||||
|
return struct {
|
||||||
|
jsonrpc: []const u8 = "1.0",
|
||||||
|
id: u64,
|
||||||
|
method: []const u8,
|
||||||
|
params: MethodArgs(m),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn RpcRespType(comptime m: MethodTag) type {
|
||||||
|
return struct {
|
||||||
|
id: u64,
|
||||||
|
result: ?ResultType(m), // keep field name or modify free fn in CallRetType
|
||||||
|
@"error": ?struct {
|
||||||
|
code: isize,
|
||||||
|
//message: []const u8, // no use for this atm
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatreq(self: *Client, comptime m: MethodTag, args: MethodArgs(m)) ![]const u8 {
|
||||||
|
const req = RpcReqType(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);
|
||||||
|
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\r\n", .{jreq.items.len});
|
||||||
|
try w.writeAll(jreq.items);
|
||||||
|
return 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);
|
||||||
|
var 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 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,
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const base64enc = std.base64.standard.Encoder;
|
||||||
|
const bitcoinrpc = @import("bitcoindrpc");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
//var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
//defer arena_state.deinit();
|
||||||
|
//const arena = arena_state.allocator();
|
||||||
|
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer if (gpa_state.deinit()) {
|
||||||
|
std.debug.print("!!!!!!!!!! memory leaks detected", .{});
|
||||||
|
};
|
||||||
|
const arena = gpa_state.allocator();
|
||||||
|
|
||||||
|
var client = bitcoinrpc.Client{
|
||||||
|
.allocator = arena,
|
||||||
|
.cookiepath = "/ssd/bitcoind/mainnet/.cookie",
|
||||||
|
};
|
||||||
|
|
||||||
|
const hash = try client.call(.getblockhash, .{ .height = 0 });
|
||||||
|
defer hash.free();
|
||||||
|
std.debug.print("hash of 1001: {s}\n", .{hash.result});
|
||||||
|
|
||||||
|
const bcinfo = try client.call(.getblockchaininfo, {});
|
||||||
|
defer bcinfo.free();
|
||||||
|
std.debug.print("{any}\n", .{bcinfo.result});
|
||||||
|
|
||||||
|
const netinfo = try client.call(.getnetworkinfo, {});
|
||||||
|
defer netinfo.free();
|
||||||
|
std.debug.print("{any}\n", .{netinfo.result});
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
//! bitcoin main tab panel.
|
||||||
|
//! all functions assume LVGL is init'ed and ui mutex is locked on entry.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const fmt = std.fmt;
|
||||||
|
|
||||||
|
const lvgl = @import("lvgl.zig");
|
||||||
|
const comm = @import("../comm.zig");
|
||||||
|
const xfmt = @import("../xfmt.zig");
|
||||||
|
|
||||||
|
const logger = std.log.scoped(.ui);
|
||||||
|
|
||||||
|
var tab: struct {
|
||||||
|
// blockchain
|
||||||
|
currblock: *lvgl.LvObj, // label
|
||||||
|
timestamp: *lvgl.LvObj, // label
|
||||||
|
blockhash: *lvgl.LvObj, // label
|
||||||
|
// usage
|
||||||
|
diskusage: *lvgl.LvObj, // label
|
||||||
|
conn_in: *lvgl.LvObj, // label
|
||||||
|
conn_out: *lvgl.LvObj, // label
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
pub fn initTabPanel(parent: *lvgl.LvObj) !void {
|
||||||
|
parent.flexFlow(.column);
|
||||||
|
|
||||||
|
const box1 = try lvgl.createFlexObject(parent, .column);
|
||||||
|
box1.setHeightToContent();
|
||||||
|
box1.setWidth(lvgl.sizePercent(100));
|
||||||
|
const l1 = try lvgl.createLabel(box1, "BLOCKCHAIN", .{});
|
||||||
|
l1.addStyle(lvgl.nm_style_title(), .{});
|
||||||
|
|
||||||
|
tab.currblock = try lvgl.createLabel(box1, "current height: 0", .{});
|
||||||
|
tab.timestamp = try lvgl.createLabel(box1, "timestamp:", .{});
|
||||||
|
tab.blockhash = try lvgl.createLabel(box1, "block hash:", .{});
|
||||||
|
|
||||||
|
const box2 = try lvgl.createFlexObject(parent, .column);
|
||||||
|
box2.setHeightToContent();
|
||||||
|
box2.setWidth(lvgl.sizePercent(100));
|
||||||
|
const l2 = try lvgl.createLabel(box2, "USAGE", .{});
|
||||||
|
l2.addStyle(lvgl.nm_style_title(), .{});
|
||||||
|
|
||||||
|
tab.diskusage = try lvgl.createLabel(box2, "disk usage:", .{});
|
||||||
|
tab.conn_in = try lvgl.createLabel(box2, "connections in:", .{});
|
||||||
|
tab.conn_out = try lvgl.createLabel(box2, "connections out:", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
var s = try fmt.bufPrintZ(&buf, "height: {d}", .{rep.blocks});
|
||||||
|
tab.currblock.setLabelText(s);
|
||||||
|
s = try fmt.bufPrintZ(&buf, "timestamp: {}", .{xfmt.unix(rep.timestamp)});
|
||||||
|
//s = try fmt.bufPrintZ(&buf, "timestamp: {}", .{rep.timestamp});
|
||||||
|
tab.timestamp.setLabelText(s);
|
||||||
|
s = try fmt.bufPrintZ(&buf, "block hash: {s}", .{rep.hash});
|
||||||
|
tab.blockhash.setLabelText(s);
|
||||||
|
|
||||||
|
s = try fmt.bufPrintZ(&buf, "disk usage: {.1}", .{fmt.fmtIntSizeBin(rep.diskusage)});
|
||||||
|
tab.diskusage.setLabelText(s);
|
||||||
|
s = try fmt.bufPrintZ(&buf, "connections in: {d}", .{rep.conn_in});
|
||||||
|
tab.conn_in.setLabelText(s);
|
||||||
|
s = try fmt.bufPrintZ(&buf, "connections out: {d}", .{rep.conn_out});
|
||||||
|
tab.conn_out.setLabelText(s);
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
//! extra formatting utilities, missing from std.fmt.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn unix(sec: u64) std.fmt.Formatter(formatUnix) {
|
||||||
|
return .{ .data = sec };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatUnix(sec: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w: anytype) !void {
|
||||||
|
_ = fmt; // unused
|
||||||
|
_ = opts;
|
||||||
|
const epoch: std.time.epoch.EpochSeconds = .{ .secs = sec };
|
||||||
|
const daysec = epoch.getDaySeconds();
|
||||||
|
const yearday = epoch.getEpochDay().calculateYearDay();
|
||||||
|
const monthday = yearday.calculateMonthDay();
|
||||||
|
return std.fmt.format(w, "{d}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2} UTC", .{
|
||||||
|
yearday.year,
|
||||||
|
monthday.month.numeric(),
|
||||||
|
monthday.day_index + 1,
|
||||||
|
daysec.getHoursIntoDay(),
|
||||||
|
daysec.getMinutesIntoHour(),
|
||||||
|
daysec.getSecondsIntoMinute(),
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue