nd,ngui: display on-chain balance in bitcoin tab #27

Manually merged
x1ddos merged 1 commits from moarbtc into master 1 year ago

@ -150,7 +150,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
btcrpc.strip = strip;
btcrpc.addModule("bitcoindrpc", b.createModule(.{ .source_file = .{ .path = "src/nd/bitcoindrpc.zig" } }));
btcrpc.addModule("bitcoindrpc", b.createModule(.{ .source_file = .{ .path = "src/bitcoindrpc.zig" } }));
const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step);

@ -5,6 +5,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const Atomic = std.atomic.Atomic;
const base64enc = std.base64.standard.Encoder;
const types = @import("types.zig");
pub const Client = struct {
allocator: std.mem.Allocator,
cookiepath: []const u8,
@ -55,7 +57,7 @@ pub const Client = struct {
};
pub fn Result(comptime m: Method) type {
return std.json.Parsed(ResultValue(m));
return types.Deinitable(ResultValue(m));
}
pub fn ResultValue(comptime m: Method) type {
@ -133,9 +135,12 @@ pub const Client = struct {
}
fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) {
const jopt = std.json.ParseOptions{ .ignore_unknown_fields = true, .allocate = .alloc_always };
const resp = try std.json.parseFromSlice(RpcResponse(m), self.allocator, b, jopt);
var resp = try types.Deinitable(RpcResponse(m)).init(self.allocator);
errdefer resp.deinit();
resp.value = try std.json.parseFromSliceLeaky(RpcResponse(m), self.allocator, b, .{
.ignore_unknown_fields = true,
.allocate = .alloc_always,
});
if (resp.value.@"error") |errfield| {
return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
}

@ -27,7 +27,7 @@ pub const Message = union(MessageTag) {
network_report: NetworkReport,
get_network_report: GetNetworkReport,
poweroff_progress: PoweroffProgress,
bitcoind_report: BitcoindReport,
bitcoind_report: BitcoinReport,
lightning_report: LightningReport,
pub const WifiConnect = struct {
@ -55,7 +55,7 @@ pub const Message = union(MessageTag) {
};
};
pub const BitcoindReport = struct {
pub const BitcoinReport = struct {
blocks: u64,
headers: u64,
timestamp: u64, // unix epoch
@ -81,6 +81,17 @@ pub const Message = union(MessageTag) {
minfee: f32, // BTC/kvB
fullrbf: bool,
},
/// on-chain balance, all values in satoshis.
/// may not be available due to disabled wallet, if bitcoin core is used,
/// or lnd turned off/nonfunctional.
balance: ?struct {
source: enum { lnd, bitcoincore },
total: i64,
confirmed: i64,
unconfirmed: i64,
locked: i64, // output leases
reserved: i64, // for fee bumps
} = null,
};
pub const LightningReport = struct {

@ -3,6 +3,7 @@
const std = @import("std");
const types = @import("types.zig");
/// safe for concurrent use as long as Client.allocator is.
pub const Client = struct {
allocator: std.mem.Allocator,
hostname: []const u8 = "localhost",
@ -307,6 +308,7 @@ pub const PendingChannel = struct {
// local_chan_reserve_sat, remote_chan_reserve_sat, initiator, chan_status_flags, memo
};
/// on-chain balance, in satoshis.
pub const WalletBalance = struct {
total_balance: i64,
confirmed_balance: i64,

@ -15,13 +15,13 @@ const std = @import("std");
const mem = std.mem;
const time = std.time;
const bitcoindrpc = @import("../bitcoindrpc.zig");
const comm = @import("../comm.zig");
const lndhttp = @import("../lndhttp.zig");
const network = @import("network.zig");
const screen = @import("../ui/screen.zig");
const types = @import("../types.zig");
const SysService = @import("SysService.zig");
const bitcoindrpc = @import("bitcoindrpc.zig");
const lndhttp = @import("../lndhttp.zig");
const types = @import("../types.zig");
const logger = std.log.scoped(.daemon);
@ -52,11 +52,11 @@ want_wifi_scan: bool, // initiate wifi scan at the next loop cycle
network_report_ready: bool, // indicates whether the network status is ready to be sent
wifi_scan_in_progress: bool = false,
wpa_save_config_on_connected: bool = false,
// bitcoin flags
// bitcoin fields
want_bitcoind_report: bool,
bitcoin_timer: time.Timer,
bitcoin_report_interval: u64 = 1 * time.ns_per_min,
// lightning flags
// lightning fields
want_lnd_report: bool,
lnd_timer: time.Timer,
lnd_report_interval: u64 = 1 * time.ns_per_min,
@ -530,7 +530,19 @@ fn sendBitcoindReport(self: *Daemon) !void {
const mempool = try client.call(.getmempoolinfo, {});
defer mempool.deinit();
const btcrep: comm.Message.BitcoindReport = .{
const balance: ?lndhttp.WalletBalance = blk: {
var lndc = lndhttp.Client.init(.{
.allocator = self.allocator,
.tlscert_path = "/home/lnd/.lnd/tls.cert",
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
}) catch break :blk null;
defer lndc.deinit();
const res = lndc.call(.walletbalance, {}) catch break :blk null;
defer res.deinit();
break :blk res.value;
};
const btcrep: comm.Message.BitcoinReport = .{
.blocks = bcinfo.value.blocks,
.headers = bcinfo.value.headers,
.timestamp = bcinfo.value.time,
@ -554,6 +566,14 @@ fn sendBitcoindReport(self: *Daemon) !void {
.minfee = mempool.value.mempoolminfee,
.fullrbf = mempool.value.fullrbf,
},
.balance = if (balance) |bal| .{
.source = .lnd,
.total = bal.total_balance,
.confirmed = bal.confirmed_balance,
.unconfirmed = bal.unconfirmed_balance,
.locked = bal.locked_balance,
.reserved = bal.reserved_balance_anchor_chan,
} else null,
};
try comm.write(self.allocator, self.uiwriter, .{ .bitcoind_report = btcrep });

@ -140,7 +140,7 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
block_count += 1;
const now = time.timestamp();
const btcrep: comm.Message.BitcoindReport = .{
const btcrep: comm.Message.BitcoinReport = .{
.blocks = block_count,
.headers = block_count,
.timestamp = @intCast(now),
@ -162,6 +162,14 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
.minfee = 0.00004155,
.fullrbf = false,
},
.balance = .{
.source = .lnd,
.total = 800000,
.confirmed = 350000,
.unconfirmed = 350000,
.locked = 0,
.reserved = 100000,
},
};
comm.write(gpa, w, .{ .bitcoind_report = btcrep }) catch |err| logger.err("comm.write: {any}", .{err});

@ -18,10 +18,17 @@ var tab: struct {
currblock: lvgl.Label,
timestamp: lvgl.Label,
blockhash: lvgl.Label,
// usage section
diskusage: lvgl.Label,
conn_in: lvgl.Label,
conn_out: lvgl.Label,
balance: struct {
avail_bar: lvgl.Bar,
avail_pct: lvgl.Label,
total: lvgl.Label,
unconf: lvgl.Label,
locked: lvgl.Label,
reserved: lvgl.Label,
},
// mempool section
mempool: struct {
txcount: lvgl.Label,
@ -41,62 +48,104 @@ pub fn initTabPanel(cont: lvgl.Container) !void {
const card = try lvgl.Card.new(parent, "BLOCKCHAIN");
const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
row.clearFlag(.scrollable);
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50));
left.setHeightToContent();
left.setPad(10, .row, .{});
tab.currblock = try lvgl.Label.new(left, "HEIGHT\n", .{ .recolor = true });
tab.timestamp = try lvgl.Label.new(left, "TIMESTAMP\n", .{ .recolor = true });
tab.blockhash = try lvgl.Label.new(row, "BLOCK HASH\n", .{ .recolor = true });
tab.blockhash.flexGrow(1);
tab.blockhash = try lvgl.Label.new(left, "BLOCK HASH\n", .{ .recolor = true });
// right column
const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50));
right.setHeightToContent();
right.setPad(10, .row, .{});
tab.diskusage = try lvgl.Label.new(right, "DISK USAGE\n", .{ .recolor = true });
tab.conn_in = try lvgl.Label.new(right, "CONNECTIONS IN\n", .{ .recolor = true });
tab.conn_out = try lvgl.Label.new(right, "CONNECTIONS OUT\n", .{ .recolor = true });
}
// mempool section
// balance section
{
const card = try lvgl.Card.new(parent, "MEMPOOL");
const card = try lvgl.Card.new(parent, "ON-CHAIN BALANCE");
const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
row.clearFlag(.scrollable);
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50));
left.setPad(8, .top, .{});
left.setPad(10, .row, .{});
tab.mempool.usage_bar = try lvgl.Bar.new(left);
tab.mempool.usage_lab = try lvgl.Label.new(left, "0Mb out of 0Mb (0%)", .{ .recolor = true });
tab.balance.avail_bar = try lvgl.Bar.new(left);
tab.balance.avail_pct = try lvgl.Label.new(left, "AVAILABLE\n", .{ .recolor = true });
tab.balance.total = try lvgl.Label.new(left, "TOTAL\n", .{ .recolor = true });
// right column
const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50));
right.setHeightToContent();
right.setPad(10, .row, .{});
tab.mempool.txcount = try lvgl.Label.new(right, "TRANSACTIONS COUNT\n", .{ .recolor = true });
tab.mempool.totalfee = try lvgl.Label.new(right, "TOTAL FEES\n", .{ .recolor = true });
tab.balance.locked = try lvgl.Label.new(right, "LOCKED\n", .{ .recolor = true });
tab.balance.reserved = try lvgl.Label.new(right, "RESERVED\n", .{ .recolor = true });
tab.balance.unconf = try lvgl.Label.new(right, "UNCONFIRMED\n", .{ .recolor = true });
}
// usage section
// mempool section
{
const card = try lvgl.Card.new(parent, "USAGE");
const card = try lvgl.Card.new(parent, "MEMPOOL");
const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.clearFlag(.scrollable);
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50));
left.setPad(8, .top, .{});
left.setPad(10, .row, .{});
tab.diskusage = try lvgl.Label.new(left, "DISK USAGE\n", .{ .recolor = true });
tab.mempool.usage_bar = try lvgl.Bar.new(left);
tab.mempool.usage_lab = try lvgl.Label.new(left, "0Mb out of 0Mb (0%)", .{ .recolor = true });
const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50));
right.setPad(10, .row, .{});
tab.conn_in = try lvgl.Label.new(right, "CONNECTIONS IN\n", .{ .recolor = true });
tab.conn_out = try lvgl.Label.new(right, "CONNECTIONS OUT\n", .{ .recolor = true });
tab.mempool.txcount = try lvgl.Label.new(right, "TRANSACTIONS COUNT\n", .{ .recolor = true });
tab.mempool.totalfee = try lvgl.Label.new(right, "TOTAL FEES\n", .{ .recolor = true });
}
}
/// updates the tab with new data from the report.
/// the tab must be inited first with initTabPanel.
pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void {
pub fn updateTabPanel(rep: comm.Message.BitcoinReport) !void {
var buf: [512]u8 = undefined;
// blockchain section
try tab.currblock.setTextFmt(&buf, cmark ++ "HEIGHT#\n{d}", .{rep.blocks});
try tab.timestamp.setTextFmt(&buf, cmark ++ "TIMESTAMP#\n{}", .{xfmt.unix(rep.timestamp)});
try tab.blockhash.setTextFmt(&buf, cmark ++ "BLOCK HASH#\n{s}\n{s}", .{ rep.hash[0..32], rep.hash[32..] });
try tab.diskusage.setTextFmt(&buf, cmark ++ "DISK USAGE#\n{:.1}", .{fmt.fmtIntSizeBin(rep.diskusage)});
try tab.conn_in.setTextFmt(&buf, cmark ++ "CONNECTIONS IN#\n{d}", .{rep.conn_in});
try tab.conn_out.setTextFmt(&buf, cmark ++ "CONNECTIONS OUT#\n{d}", .{rep.conn_out});
// balance section
if (rep.balance) |bal| {
const confpct: f32 = pct: {
if (bal.confirmed > bal.total) {
break :pct 100;
}
if (bal.total == 0) {
break :pct 0;
}
const v = @as(f64, @floatFromInt(bal.confirmed)) / @as(f64, @floatFromInt(bal.total));
break :pct @floatCast(v * 100);
};
tab.balance.avail_bar.setValue(@as(i32, @intFromFloat(@round(confpct))));
try tab.balance.avail_pct.setTextFmt(&buf, cmark ++ "AVAILABLE#\n{} sat ({d:.1}%)", .{
xfmt.imetric(bal.confirmed),
confpct,
});
try tab.balance.total.setTextFmt(&buf, cmark ++ "TOTAL#\n{} sat", .{xfmt.imetric(bal.total)});
try tab.balance.unconf.setTextFmt(&buf, cmark ++ "UNCONFIRMED#\n{} sat", .{xfmt.imetric(bal.unconfirmed)});
try tab.balance.locked.setTextFmt(&buf, cmark ++ "LOCKED#\n{} sat", .{xfmt.imetric(bal.locked)});
try tab.balance.reserved.setTextFmt(&buf, cmark ++ "RESERVED#\n{} sat", .{xfmt.imetric(bal.reserved)});
}
// mempool section
const mempool_pct: f32 = pct: {
@ -117,9 +166,4 @@ pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void {
});
try tab.mempool.txcount.setTextFmt(&buf, cmark ++ "TRANSACTIONS COUNT#\n{d}", .{rep.mempool.txcount});
try tab.mempool.totalfee.setTextFmt(&buf, cmark ++ "TOTAL FEES#\n{d:10} BTC", .{rep.mempool.totalfee});
// usage section
try tab.diskusage.setTextFmt(&buf, cmark ++ "DISK USAGE#\n{:.1}", .{fmt.fmtIntSizeBin(rep.diskusage)});
try tab.conn_in.setTextFmt(&buf, cmark ++ "CONNECTIONS IN#\n{d}", .{rep.conn_in});
try tab.conn_out.setTextFmt(&buf, cmark ++ "CONNECTIONS OUT#\n{d}", .{rep.conn_out});
}