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,
});
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});
}