From 116fb3b59cd5eda545992e9956f16ba10465a04f Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 25 Aug 2023 09:24:40 +0200 Subject: [PATCH] nd,ngui: display on-chain balance in bitcoin tab 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. --- build.zig | 2 +- src/{nd => }/bitcoindrpc.zig | 11 +++-- src/comm.zig | 15 +++++- src/lndhttp.zig | 2 + src/nd/Daemon.zig | 32 ++++++++++--- src/test/guiplay.zig | 10 +++- src/ui/bitcoin.zig | 88 +++++++++++++++++++++++++++--------- 7 files changed, 125 insertions(+), 35 deletions(-) rename src/{nd => }/bitcoindrpc.zig (96%) diff --git a/build.zig b/build.zig index 4339a32..529fdc3 100644 --- a/build.zig +++ b/build.zig @@ -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); diff --git a/src/nd/bitcoindrpc.zig b/src/bitcoindrpc.zig similarity index 96% rename from src/nd/bitcoindrpc.zig rename to src/bitcoindrpc.zig index 670a28e..a813c6a 100644 --- a/src/nd/bitcoindrpc.zig +++ b/src/bitcoindrpc.zig @@ -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; } diff --git a/src/comm.zig b/src/comm.zig index 2ecf273..987dfed 100644 --- a/src/comm.zig +++ b/src/comm.zig @@ -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 { diff --git a/src/lndhttp.zig b/src/lndhttp.zig index ccac0ab..db517f6 100644 --- a/src/lndhttp.zig +++ b/src/lndhttp.zig @@ -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, diff --git a/src/nd/Daemon.zig b/src/nd/Daemon.zig index 9a01662..2a91ef2 100644 --- a/src/nd/Daemon.zig +++ b/src/nd/Daemon.zig @@ -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 }); diff --git a/src/test/guiplay.zig b/src/test/guiplay.zig index d8802e5..1e7d144 100644 --- a/src/test/guiplay.zig +++ b/src/test/guiplay.zig @@ -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}); diff --git a/src/ui/bitcoin.zig b/src/ui/bitcoin.zig index 3ae53d5..efda243 100644 --- a/src/ui/bitcoin.zig +++ b/src/ui/bitcoin.zig @@ -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}); }