nd,ngui: display on-chain balance in bitcoin tab
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details

a previous commit added some lightning tab implementation which
including balance details but only for lightning channels.

this commit queries lnd for a wallet balance and displays it on the
bitcoin tab since "wallet" funds are on-chain and it doesn't feel like
it belongs to the lightning tab in the UI.

while there, also improved some daemon backend code style, alightning
with the lightning implementation structures.
pull/27/head v0.4.0
alex 1 year ago
parent 05c89bbd1c
commit 116fb3b59c
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -150,7 +150,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
btcrpc.strip = strip; 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"); const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step); btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step);

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

@ -27,7 +27,7 @@ pub const Message = union(MessageTag) {
network_report: NetworkReport, network_report: NetworkReport,
get_network_report: GetNetworkReport, get_network_report: GetNetworkReport,
poweroff_progress: PoweroffProgress, poweroff_progress: PoweroffProgress,
bitcoind_report: BitcoindReport, bitcoind_report: BitcoinReport,
lightning_report: LightningReport, lightning_report: LightningReport,
pub const WifiConnect = struct { pub const WifiConnect = struct {
@ -55,7 +55,7 @@ pub const Message = union(MessageTag) {
}; };
}; };
pub const BitcoindReport = struct { pub const BitcoinReport = struct {
blocks: u64, blocks: u64,
headers: u64, headers: u64,
timestamp: u64, // unix epoch timestamp: u64, // unix epoch
@ -81,6 +81,17 @@ pub const Message = union(MessageTag) {
minfee: f32, // BTC/kvB minfee: f32, // BTC/kvB
fullrbf: bool, 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 { pub const LightningReport = struct {

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

@ -15,13 +15,13 @@ const std = @import("std");
const mem = std.mem; const mem = std.mem;
const time = std.time; const time = std.time;
const bitcoindrpc = @import("../bitcoindrpc.zig");
const comm = @import("../comm.zig"); const comm = @import("../comm.zig");
const lndhttp = @import("../lndhttp.zig");
const network = @import("network.zig"); const network = @import("network.zig");
const screen = @import("../ui/screen.zig"); const screen = @import("../ui/screen.zig");
const types = @import("../types.zig");
const SysService = @import("SysService.zig"); const SysService = @import("SysService.zig");
const bitcoindrpc = @import("bitcoindrpc.zig"); const types = @import("../types.zig");
const lndhttp = @import("../lndhttp.zig");
const logger = std.log.scoped(.daemon); 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 network_report_ready: bool, // indicates whether the network status is ready to be sent
wifi_scan_in_progress: bool = false, wifi_scan_in_progress: bool = false,
wpa_save_config_on_connected: bool = false, wpa_save_config_on_connected: bool = false,
// bitcoin flags // bitcoin fields
want_bitcoind_report: bool, want_bitcoind_report: bool,
bitcoin_timer: time.Timer, bitcoin_timer: time.Timer,
bitcoin_report_interval: u64 = 1 * time.ns_per_min, bitcoin_report_interval: u64 = 1 * time.ns_per_min,
// lightning flags // lightning fields
want_lnd_report: bool, want_lnd_report: bool,
lnd_timer: time.Timer, lnd_timer: time.Timer,
lnd_report_interval: u64 = 1 * time.ns_per_min, lnd_report_interval: u64 = 1 * time.ns_per_min,
@ -530,7 +530,19 @@ fn sendBitcoindReport(self: *Daemon) !void {
const mempool = try client.call(.getmempoolinfo, {}); const mempool = try client.call(.getmempoolinfo, {});
defer mempool.deinit(); 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, .blocks = bcinfo.value.blocks,
.headers = bcinfo.value.headers, .headers = bcinfo.value.headers,
.timestamp = bcinfo.value.time, .timestamp = bcinfo.value.time,
@ -554,6 +566,14 @@ fn sendBitcoindReport(self: *Daemon) !void {
.minfee = mempool.value.mempoolminfee, .minfee = mempool.value.mempoolminfee,
.fullrbf = mempool.value.fullrbf, .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 }); 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; block_count += 1;
const now = time.timestamp(); const now = time.timestamp();
const btcrep: comm.Message.BitcoindReport = .{ const btcrep: comm.Message.BitcoinReport = .{
.blocks = block_count, .blocks = block_count,
.headers = block_count, .headers = block_count,
.timestamp = @intCast(now), .timestamp = @intCast(now),
@ -162,6 +162,14 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
.minfee = 0.00004155, .minfee = 0.00004155,
.fullrbf = false, .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}); 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, currblock: lvgl.Label,
timestamp: lvgl.Label, timestamp: lvgl.Label,
blockhash: lvgl.Label, blockhash: lvgl.Label,
// usage section
diskusage: lvgl.Label, diskusage: lvgl.Label,
conn_in: lvgl.Label, conn_in: lvgl.Label,
conn_out: 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 section
mempool: struct { mempool: struct {
txcount: lvgl.Label, txcount: lvgl.Label,
@ -41,62 +48,104 @@ pub fn initTabPanel(cont: lvgl.Container) !void {
const card = try lvgl.Card.new(parent, "BLOCKCHAIN"); const card = try lvgl.Card.new(parent, "BLOCKCHAIN");
const row = try lvgl.FlexLayout.new(card, .row, .{}); const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100)); row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
row.clearFlag(.scrollable); row.clearFlag(.scrollable);
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{}); const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50)); left.setWidth(lvgl.sizePercent(50));
left.setHeightToContent();
left.setPad(10, .row, .{}); left.setPad(10, .row, .{});
tab.currblock = try lvgl.Label.new(left, "HEIGHT\n", .{ .recolor = true }); tab.currblock = try lvgl.Label.new(left, "HEIGHT\n", .{ .recolor = true });
tab.timestamp = try lvgl.Label.new(left, "TIMESTAMP\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 = try lvgl.Label.new(left, "BLOCK HASH\n", .{ .recolor = true });
tab.blockhash.flexGrow(1); // 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 });
} }
// balance section
// mempool 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, .{}); const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100)); row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
row.clearFlag(.scrollable); row.clearFlag(.scrollable);
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{}); const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50)); left.setWidth(lvgl.sizePercent(50));
left.setPad(8, .top, .{}); left.setPad(8, .top, .{});
left.setPad(10, .row, .{}); left.setPad(10, .row, .{});
tab.mempool.usage_bar = try lvgl.Bar.new(left); tab.balance.avail_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_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, .{}); const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50)); right.setWidth(lvgl.sizePercent(50));
right.setHeightToContent();
right.setPad(10, .row, .{}); right.setPad(10, .row, .{});
tab.mempool.txcount = try lvgl.Label.new(right, "TRANSACTIONS COUNT\n", .{ .recolor = true }); tab.balance.locked = try lvgl.Label.new(right, "LOCKED\n", .{ .recolor = true });
tab.mempool.totalfee = try lvgl.Label.new(right, "TOTAL FEES\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 });
} }
// mempool section
// usage 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, .{}); const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100)); row.setWidth(lvgl.sizePercent(100));
row.clearFlag(.scrollable); row.clearFlag(.scrollable);
const left = try lvgl.FlexLayout.new(row, .column, .{}); const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50)); left.setWidth(lvgl.sizePercent(50));
left.setPad(8, .top, .{});
left.setPad(10, .row, .{}); 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, .{}); const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50)); right.setWidth(lvgl.sizePercent(50));
right.setPad(10, .row, .{}); right.setPad(10, .row, .{});
tab.conn_in = try lvgl.Label.new(right, "CONNECTIONS IN\n", .{ .recolor = true }); tab.mempool.txcount = try lvgl.Label.new(right, "TRANSACTIONS COUNT\n", .{ .recolor = true });
tab.conn_out = try lvgl.Label.new(right, "CONNECTIONS OUT\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. /// updates the tab with new data from the report.
/// the tab must be inited first with initTabPanel. /// 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; var buf: [512]u8 = undefined;
// blockchain section // blockchain section
try tab.currblock.setTextFmt(&buf, cmark ++ "HEIGHT#\n{d}", .{rep.blocks}); 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.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.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 // mempool section
const mempool_pct: f32 = pct: { 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.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}); 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});
} }