@ -0,0 +1,327 @@
|
|||||||
|
//! lnd lightning HTTP client and utility functions.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const types = @import("types.zig");
|
||||||
|
|
||||||
|
pub const Client = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
hostname: []const u8 = "localhost",
|
||||||
|
port: u16 = 10010,
|
||||||
|
apibase: []const u8, // https://localhost:10010
|
||||||
|
macaroon: struct {
|
||||||
|
readonly: []const u8,
|
||||||
|
admin: ?[]const u8,
|
||||||
|
},
|
||||||
|
httpClient: std.http.Client,
|
||||||
|
|
||||||
|
pub const ApiMethod = enum {
|
||||||
|
feereport, // fees of all active channels
|
||||||
|
getinfo, // general host node info
|
||||||
|
getnetworkinfo, // visible graph info
|
||||||
|
listchannels, // active channels
|
||||||
|
pendingchannels, // pending open/close channels
|
||||||
|
walletbalance, // onchain balance
|
||||||
|
walletstatus, // server/wallet status
|
||||||
|
// fwdinghistory, getchaninfo, getnodeinfo
|
||||||
|
// genseed, initwallet, unlockwallet
|
||||||
|
// watchtower: getinfo, stats, list, add, remove
|
||||||
|
|
||||||
|
fn apipath(self: @This()) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.feereport => "v1/fees",
|
||||||
|
.getinfo => "v1/getinfo",
|
||||||
|
.getnetworkinfo => "v1/graph/info",
|
||||||
|
.listchannels => "v1/channels",
|
||||||
|
.pendingchannels => "v1/channels/pending",
|
||||||
|
.walletbalance => "v1/balance/blockchain",
|
||||||
|
.walletstatus => "v1/state",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn MethodArgs(comptime m: ApiMethod) type {
|
||||||
|
return switch (m) {
|
||||||
|
.listchannels => struct {
|
||||||
|
status: ?enum { active, inactive } = null,
|
||||||
|
advert: ?enum { public, private } = null,
|
||||||
|
peer: ?[]const u8 = null, // hex pubkey; filter out non-matching peers
|
||||||
|
peer_alias_lookup: bool, // performance penalty if set to true
|
||||||
|
},
|
||||||
|
else => void,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ResultValue(comptime m: ApiMethod) type {
|
||||||
|
return switch (m) {
|
||||||
|
.feereport => FeeReport,
|
||||||
|
.getinfo => LndInfo,
|
||||||
|
.getnetworkinfo => NetworkInfo,
|
||||||
|
.listchannels => ChannelsList,
|
||||||
|
.pendingchannels => PendingList,
|
||||||
|
.walletbalance => WalletBalance,
|
||||||
|
.walletstatus => WalletStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const InitOpt = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
hostname: []const u8 = "localhost", // must be present in tlscert_path SANs
|
||||||
|
port: u16 = 10010, // HTTP API port
|
||||||
|
tlscert_path: []const u8, // must contain the hostname in SANs
|
||||||
|
macaroon_ro_path: []const u8, // readonly macaroon path
|
||||||
|
macaroon_admin_path: ?[]const u8 = null, // required only for requests mutating lnd state
|
||||||
|
};
|
||||||
|
|
||||||
|
/// opt slices are dup'ed and need not be kept alive.
|
||||||
|
/// must deinit when done.
|
||||||
|
pub fn init(opt: InitOpt) !Client {
|
||||||
|
var ca = std.crypto.Certificate.Bundle{}; // deinit'ed by http.Client.deinit
|
||||||
|
errdefer ca.deinit(opt.allocator);
|
||||||
|
try ca.addCertsFromFilePathAbsolute(opt.allocator, opt.tlscert_path);
|
||||||
|
const apibase = try std.fmt.allocPrint(opt.allocator, "https://{s}:{d}", .{ opt.hostname, opt.port });
|
||||||
|
errdefer opt.allocator.free(apibase);
|
||||||
|
const mac_ro = try readMacaroon(opt.allocator, opt.macaroon_ro_path);
|
||||||
|
errdefer opt.allocator.free(mac_ro);
|
||||||
|
const mac_admin = if (opt.macaroon_admin_path) |p| try readMacaroon(opt.allocator, p) else null;
|
||||||
|
return .{
|
||||||
|
.allocator = opt.allocator,
|
||||||
|
.apibase = apibase,
|
||||||
|
.macaroon = .{ .readonly = mac_ro, .admin = mac_admin },
|
||||||
|
.httpClient = std.http.Client{
|
||||||
|
.allocator = opt.allocator,
|
||||||
|
.ca_bundle = ca,
|
||||||
|
.next_https_rescan_certs = false, // use only the provided CA bundle above
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Client) void {
|
||||||
|
self.httpClient.deinit();
|
||||||
|
self.allocator.free(self.apibase);
|
||||||
|
self.allocator.free(self.macaroon.readonly);
|
||||||
|
if (self.macaroon.admin) |a| {
|
||||||
|
self.allocator.free(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Result(comptime m: ApiMethod) type {
|
||||||
|
return if (@TypeOf(ResultValue(m)) == void) void else types.Deinitable(ResultValue(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call(self: *Client, comptime apimethod: ApiMethod, args: MethodArgs(apimethod)) !Result(apimethod) {
|
||||||
|
const formatted = try self.formatreq(apimethod, args);
|
||||||
|
defer formatted.deinit();
|
||||||
|
const reqinfo = formatted.value;
|
||||||
|
const opt = std.http.Client.Options{ .handle_redirects = false }; // no redirects in REST API
|
||||||
|
var req = try self.httpClient.request(reqinfo.httpmethod, reqinfo.url, reqinfo.headers, opt);
|
||||||
|
defer req.deinit();
|
||||||
|
try req.start();
|
||||||
|
if (reqinfo.payload) |p| {
|
||||||
|
try req.writer().writeAll(p);
|
||||||
|
try req.finish();
|
||||||
|
}
|
||||||
|
try req.wait();
|
||||||
|
if (req.response.status.class() != .success) {
|
||||||
|
return error.LndHttpBadStatusCode;
|
||||||
|
}
|
||||||
|
if (@TypeOf(Result(apimethod)) == void) {
|
||||||
|
return; // void response; need no json parsing
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = try req.reader().readAllAlloc(self.allocator, 1 << 20); // 1Mb should be enough for all response types
|
||||||
|
defer self.allocator.free(body);
|
||||||
|
var res = try Result(apimethod).init(self.allocator);
|
||||||
|
errdefer res.deinit();
|
||||||
|
res.value = try std.json.parseFromSliceLeaky(ResultValue(apimethod), res.arena.allocator(), body, .{
|
||||||
|
.ignore_unknown_fields = true,
|
||||||
|
.allocate = .alloc_always,
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HttpReqInfo = struct {
|
||||||
|
httpmethod: std.http.Method,
|
||||||
|
url: std.Uri,
|
||||||
|
headers: std.http.Headers,
|
||||||
|
payload: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn formatreq(self: Client, comptime apimethod: ApiMethod, args: MethodArgs(apimethod)) !types.Deinitable(HttpReqInfo) {
|
||||||
|
const authHeaderName = "grpc-metadata-macaroon";
|
||||||
|
var reqinfo = try types.Deinitable(HttpReqInfo).init(self.allocator);
|
||||||
|
errdefer reqinfo.deinit();
|
||||||
|
const arena = reqinfo.arena.allocator();
|
||||||
|
reqinfo.value = switch (apimethod) {
|
||||||
|
.feereport, .getinfo, .getnetworkinfo, .pendingchannels, .walletbalance, .walletstatus => |m| .{
|
||||||
|
.httpmethod = .GET,
|
||||||
|
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
||||||
|
.headers = blk: {
|
||||||
|
var h = std.http.Headers{ .allocator = arena };
|
||||||
|
try h.append(authHeaderName, self.macaroon.readonly);
|
||||||
|
break :blk h;
|
||||||
|
},
|
||||||
|
.payload = null,
|
||||||
|
},
|
||||||
|
.listchannels => .{
|
||||||
|
.httpmethod = .GET,
|
||||||
|
.url = blk: {
|
||||||
|
var buf = std.ArrayList(u8).init(arena);
|
||||||
|
const w = buf.writer();
|
||||||
|
try std.fmt.format(w, "{s}/v1/channels?peer_alias_lookup={}", .{ self.apibase, args.peer_alias_lookup });
|
||||||
|
if (args.status) |v| switch (v) {
|
||||||
|
.active => try w.writeAll("&active_only=true"),
|
||||||
|
.inactive => try w.writeAll("&inactive_only=true"),
|
||||||
|
};
|
||||||
|
if (args.advert) |v| switch (v) {
|
||||||
|
.public => try w.writeAll("&public_only=true"),
|
||||||
|
.private => try w.writeAll("&private_only=true"),
|
||||||
|
};
|
||||||
|
if (args.peer) |v| {
|
||||||
|
// TODO: sanitize; Uri.writeEscapedQuery(w, q);
|
||||||
|
try std.fmt.format(w, "&peer={s}", .{v});
|
||||||
|
}
|
||||||
|
break :blk try std.Uri.parse(buf.items);
|
||||||
|
},
|
||||||
|
.headers = blk: {
|
||||||
|
var h = std.http.Headers{ .allocator = arena };
|
||||||
|
try h.append(authHeaderName, self.macaroon.readonly);
|
||||||
|
break :blk h;
|
||||||
|
},
|
||||||
|
.payload = null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return reqinfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// callers own returned value.
|
||||||
|
fn readMacaroon(gpa: std.mem.Allocator, path: []const u8) ![]const u8 {
|
||||||
|
const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only });
|
||||||
|
defer file.close();
|
||||||
|
const cont = try file.readToEndAlloc(gpa, 1024);
|
||||||
|
defer gpa.free(cont);
|
||||||
|
return std.fmt.allocPrint(gpa, "{}", .{std.fmt.fmtSliceHexLower(cont)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// general info and stats around the host lnd.
|
||||||
|
pub const LndInfo = struct {
|
||||||
|
version: []const u8,
|
||||||
|
identity_pubkey: []const u8,
|
||||||
|
alias: []const u8,
|
||||||
|
color: []const u8,
|
||||||
|
num_pending_channels: u32,
|
||||||
|
num_active_channels: u32,
|
||||||
|
num_inactive_channels: u32,
|
||||||
|
num_peers: u32,
|
||||||
|
block_height: u32,
|
||||||
|
block_hash: []const u8,
|
||||||
|
synced_to_chain: bool,
|
||||||
|
synced_to_graph: bool,
|
||||||
|
chains: []const struct {
|
||||||
|
chain: []const u8,
|
||||||
|
network: []const u8,
|
||||||
|
},
|
||||||
|
uris: []const []const u8,
|
||||||
|
// best_header_timestamp and features?
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NetworkInfo = struct {
|
||||||
|
graph_diameter: u32,
|
||||||
|
avg_out_degree: f32,
|
||||||
|
max_out_degree: u32,
|
||||||
|
num_nodes: u32,
|
||||||
|
num_channels: u32,
|
||||||
|
total_network_capacity: i64,
|
||||||
|
avg_channel_size: f64,
|
||||||
|
min_channel_size: i64,
|
||||||
|
max_channel_size: i64,
|
||||||
|
median_channel_size_sat: i64,
|
||||||
|
num_zombie_chans: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FeeReport = struct {
|
||||||
|
day_fee_sum: u64,
|
||||||
|
week_fee_sum: u64,
|
||||||
|
month_fee_sum: u64,
|
||||||
|
channel_fees: []struct {
|
||||||
|
chan_id: []const u8,
|
||||||
|
channel_point: []const u8,
|
||||||
|
base_fee_msat: i64,
|
||||||
|
fee_per_mil: i64, // per milli-satoshis, in millionths of satoshi
|
||||||
|
fee_rate: f64, // fee_per_mil/10^6, in milli-satoshis
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ChannelsList = struct {
|
||||||
|
channels: []struct {
|
||||||
|
chan_id: []const u8, // [0..3]: height, [3..6]: index within block, [6..8]: chan out idx
|
||||||
|
remote_pubkey: []const u8,
|
||||||
|
channel_point: []const u8, // txid:index of the funding tx
|
||||||
|
capacity: i64,
|
||||||
|
local_balance: i64,
|
||||||
|
remote_balance: i64,
|
||||||
|
unsettled_balance: i64,
|
||||||
|
total_satoshis_sent: i64,
|
||||||
|
total_satoshis_received: i64,
|
||||||
|
active: bool,
|
||||||
|
private: bool,
|
||||||
|
initiator: bool,
|
||||||
|
peer_alias: []const u8,
|
||||||
|
// https://github.com/lightningnetwork/lnd/blob/d930dcec/channeldb/channel.go#L616-L644
|
||||||
|
//chan_status_flag: ChannelStatus
|
||||||
|
//local_constraints, remote_constraints, pending_htlcs
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PendingList = struct {
|
||||||
|
total_limbo_balance: i64, // balance in satoshis encumbered in pending channels
|
||||||
|
pending_open_channels: []struct {
|
||||||
|
channel: PendingChannel,
|
||||||
|
commit_fee: i64,
|
||||||
|
funding_expiry_blocks: i32,
|
||||||
|
},
|
||||||
|
pending_force_closing_channels: []struct {
|
||||||
|
channel: PendingChannel,
|
||||||
|
closing_txid: []const u8,
|
||||||
|
limbo_balance: i64,
|
||||||
|
maturity_height: u32,
|
||||||
|
blocks_til_maturity: i32, // negative indicates n blocks since maturity
|
||||||
|
recovered_balance: i64, // total funds successfully recovered from this channel
|
||||||
|
// pending_htlcs, anchor
|
||||||
|
},
|
||||||
|
waiting_close_channels: []struct { // waiting for closing tx confirmation
|
||||||
|
channel: PendingChannel,
|
||||||
|
limbo_balance: i64,
|
||||||
|
closing_txid: []const u8,
|
||||||
|
// commitments?
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PendingChannel = struct {
|
||||||
|
remote_node_pub: []const u8,
|
||||||
|
channel_point: []const u8,
|
||||||
|
capacity: i64,
|
||||||
|
local_balance: i64,
|
||||||
|
remote_balance: i64,
|
||||||
|
private: bool,
|
||||||
|
// local_chan_reserve_sat, remote_chan_reserve_sat, initiator, chan_status_flags, memo
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WalletBalance = struct {
|
||||||
|
total_balance: i64,
|
||||||
|
confirmed_balance: i64,
|
||||||
|
unconfirmed_balance: i64,
|
||||||
|
locked_balance: i64, // output leases
|
||||||
|
reserved_balance_anchor_chan: i64, // for fee bumps
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WalletStatus = struct {
|
||||||
|
state: enum(u8) {
|
||||||
|
NON_EXISTING = 0, // uninitialized
|
||||||
|
LOCKED = 1, // requires password to unlocked
|
||||||
|
UNLOCKED = 2, // RPC isn't ready
|
||||||
|
RPC_ACTIVE = 3, // lnd server active but not ready for calls yet
|
||||||
|
SERVER_ACTIVE = 4, // ready to accept calls
|
||||||
|
WAITING_TO_START = 255,
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const lndhttp = @import("lndhttp");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer if (gpa_state.deinit() == .leak) {
|
||||||
|
std.debug.print("memory leaks detected!", .{});
|
||||||
|
};
|
||||||
|
const gpa = gpa_state.allocator();
|
||||||
|
|
||||||
|
var client = try lndhttp.Client.init(.{
|
||||||
|
.allocator = gpa,
|
||||||
|
.tlscert_path = "/home/lnd/.lnd/tls.cert",
|
||||||
|
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
|
||||||
|
});
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = try client.call(.getinfo, {});
|
||||||
|
defer res.deinit();
|
||||||
|
std.debug.print("{any}\n", .{res.value});
|
||||||
|
}
|
||||||
|
//{
|
||||||
|
// const res = try client.call(.getnetworkinfo, {});
|
||||||
|
// defer res.deinit();
|
||||||
|
// std.debug.print("{any}\n", .{res.value});
|
||||||
|
//}
|
||||||
|
//{
|
||||||
|
// const res = try client.call(.listchannels, .{ .peer_alias_lookup = false });
|
||||||
|
// defer res.deinit();
|
||||||
|
// std.debug.print("{any}\n", .{res.value.channels});
|
||||||
|
//}
|
||||||
|
//{
|
||||||
|
// const res = try client.call(.walletstatus, {});
|
||||||
|
// defer res.deinit();
|
||||||
|
// std.debug.print("{s}\n", .{@tagName(res.value.state)});
|
||||||
|
//}
|
||||||
|
//{
|
||||||
|
// const res = try client.call(.feereport, {});
|
||||||
|
// defer res.deinit();
|
||||||
|
// std.debug.print("{any}\n", .{res.value});
|
||||||
|
//}
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
//! lightning main tab panel and other functionality.
|
||||||
|
//! all functions assume LVGL is init'ed and ui mutex is locked on entry.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const comm = @import("../comm.zig");
|
||||||
|
const lvgl = @import("lvgl.zig");
|
||||||
|
const xfmt = @import("../xfmt.zig");
|
||||||
|
|
||||||
|
const logger = std.log.scoped(.ui_lnd);
|
||||||
|
/// label color mark start to make "label:" part of a "label: value"
|
||||||
|
/// in a different color.
|
||||||
|
const cmark = "#bbbbbb ";
|
||||||
|
|
||||||
|
var tab: struct {
|
||||||
|
info: struct {
|
||||||
|
alias: lvgl.Label,
|
||||||
|
blockhash: lvgl.Label,
|
||||||
|
currblock: lvgl.Label,
|
||||||
|
npeers: lvgl.Label,
|
||||||
|
pubkey: lvgl.Label,
|
||||||
|
version: lvgl.Label,
|
||||||
|
},
|
||||||
|
balance: struct {
|
||||||
|
avail: lvgl.Bar, // local vs remote
|
||||||
|
local: lvgl.Label,
|
||||||
|
remote: lvgl.Label,
|
||||||
|
unsettled: lvgl.Label,
|
||||||
|
pending: lvgl.Label,
|
||||||
|
fees: lvgl.Label, // day, week, month
|
||||||
|
},
|
||||||
|
channels_cont: lvgl.FlexLayout,
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
/// creates the tab content with all elements.
|
||||||
|
/// must be called only once at UI init.
|
||||||
|
pub fn initTabPanel(cont: lvgl.Container) !void {
|
||||||
|
const parent = cont.flex(.column, .{});
|
||||||
|
|
||||||
|
// info section
|
||||||
|
{
|
||||||
|
const card = try lvgl.Card.new(parent, "INFO");
|
||||||
|
const row = try lvgl.FlexLayout.new(card, .row, .{});
|
||||||
|
row.setHeightToContent();
|
||||||
|
row.setWidth(lvgl.sizePercent(100));
|
||||||
|
row.clearFlag(.scrollable);
|
||||||
|
// left column
|
||||||
|
const left = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
|
left.setHeightToContent();
|
||||||
|
left.setWidth(lvgl.sizePercent(50));
|
||||||
|
left.setPad(10, .row, .{});
|
||||||
|
tab.info.alias = try lvgl.Label.new(left, "ALIAS\n", .{ .recolor = true });
|
||||||
|
tab.info.pubkey = try lvgl.Label.new(left, "PUBKEY\n", .{ .recolor = true });
|
||||||
|
tab.info.version = try lvgl.Label.new(left, "VERSION\n", .{ .recolor = true });
|
||||||
|
// right column
|
||||||
|
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
|
right.setHeightToContent();
|
||||||
|
right.setWidth(lvgl.sizePercent(50));
|
||||||
|
right.setPad(10, .row, .{});
|
||||||
|
tab.info.currblock = try lvgl.Label.new(right, "HEIGHT\n", .{ .recolor = true });
|
||||||
|
tab.info.blockhash = try lvgl.Label.new(right, "BLOCK HASH\n", .{ .recolor = true });
|
||||||
|
tab.info.npeers = try lvgl.Label.new(right, "CONNECTED PEERS\n", .{ .recolor = true });
|
||||||
|
}
|
||||||
|
// balance section
|
||||||
|
{
|
||||||
|
const card = try lvgl.Card.new(parent, "BALANCE");
|
||||||
|
const row = try lvgl.FlexLayout.new(card, .row, .{});
|
||||||
|
row.setWidth(lvgl.sizePercent(100));
|
||||||
|
row.clearFlag(.scrollable);
|
||||||
|
// left column
|
||||||
|
const left = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
|
left.setWidth(lvgl.sizePercent(50));
|
||||||
|
left.setPad(10, .row, .{});
|
||||||
|
tab.balance.avail = try lvgl.Bar.new(left);
|
||||||
|
tab.balance.avail.setWidth(lvgl.sizePercent(90));
|
||||||
|
const subrow = try lvgl.FlexLayout.new(left, .row, .{ .main = .space_between });
|
||||||
|
subrow.setWidth(lvgl.sizePercent(90));
|
||||||
|
subrow.setHeightToContent();
|
||||||
|
tab.balance.local = try lvgl.Label.new(subrow, "LOCAL\n", .{ .recolor = true });
|
||||||
|
tab.balance.remote = try lvgl.Label.new(subrow, "REMOTE\n", .{ .recolor = true });
|
||||||
|
// right column
|
||||||
|
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
|
right.setWidth(lvgl.sizePercent(50));
|
||||||
|
right.setPad(10, .row, .{});
|
||||||
|
tab.balance.pending = try lvgl.Label.new(right, "PENDING\n", .{ .recolor = true });
|
||||||
|
tab.balance.unsettled = try lvgl.Label.new(right, "UNSETTLED\n", .{ .recolor = true });
|
||||||
|
// bottom
|
||||||
|
tab.balance.fees = try lvgl.Label.new(card, "ACCUMULATED FORWARDING FEES\n", .{ .recolor = true });
|
||||||
|
}
|
||||||
|
// channels section
|
||||||
|
{
|
||||||
|
const card = try lvgl.Card.new(parent, "CHANNELS");
|
||||||
|
tab.channels_cont = try lvgl.FlexLayout.new(card, .column, .{});
|
||||||
|
tab.channels_cont.setHeightToContent();
|
||||||
|
tab.channels_cont.setWidth(lvgl.sizePercent(100));
|
||||||
|
tab.channels_cont.clearFlag(.scrollable);
|
||||||
|
tab.channels_cont.setPad(10, .row, .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// updates the tab with new data from the report.
|
||||||
|
/// the tab must be inited first with initTabPanel.
|
||||||
|
pub fn updateTabPanel(rep: comm.Message.LightningReport) !void {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
|
||||||
|
// info section
|
||||||
|
try tab.info.alias.setTextFmt(&buf, cmark ++ "ALIAS#\n{s}", .{rep.alias});
|
||||||
|
try tab.info.pubkey.setTextFmt(&buf, cmark ++ "PUBKEY#\n{s}\n{s}", .{ rep.pubkey[0..33], rep.pubkey[33..] });
|
||||||
|
try tab.info.version.setTextFmt(&buf, cmark ++ "VERSION#\n{s}", .{rep.version});
|
||||||
|
try tab.info.currblock.setTextFmt(&buf, cmark ++ "HEIGHT#\n{d}", .{rep.height});
|
||||||
|
try tab.info.blockhash.setTextFmt(&buf, cmark ++ "BLOCK HASH#\n{s}\n{s}", .{ rep.hash[0..32], rep.hash[32..] });
|
||||||
|
try tab.info.npeers.setTextFmt(&buf, cmark ++ "CONNECTED PEERS#\n{d}", .{rep.npeers});
|
||||||
|
|
||||||
|
// balance section
|
||||||
|
const local_pct: i32 = pct: {
|
||||||
|
const total = rep.totalbalance.local + rep.totalbalance.remote;
|
||||||
|
if (total == 0) {
|
||||||
|
break :pct 0;
|
||||||
|
}
|
||||||
|
const v = @as(f64, @floatFromInt(rep.totalbalance.local)) / @as(f64, @floatFromInt(total));
|
||||||
|
break :pct @intFromFloat(v * 100);
|
||||||
|
};
|
||||||
|
tab.balance.avail.setValue(local_pct);
|
||||||
|
try tab.balance.local.setTextFmt(&buf, cmark ++ "LOCAL#\n{} sat", .{xfmt.imetric(rep.totalbalance.local)});
|
||||||
|
try tab.balance.remote.setTextFmt(&buf, cmark ++ "REMOTE#\n{} sat", .{xfmt.imetric(rep.totalbalance.remote)});
|
||||||
|
try tab.balance.pending.setTextFmt(&buf, cmark ++ "PENDING#\n{} sat", .{xfmt.imetric(rep.totalbalance.pending)});
|
||||||
|
try tab.balance.unsettled.setTextFmt(&buf, cmark ++ "UNSETTLED#\n{}", .{xfmt.imetric(rep.totalbalance.unsettled)});
|
||||||
|
try tab.balance.fees.setTextFmt(&buf, cmark ++ "ACCUMULATED FORWARDING FEES#\nDAY: {} sat WEEK: {} sat MONTH: {} sat", .{
|
||||||
|
xfmt.umetric(rep.totalfees.day),
|
||||||
|
xfmt.umetric(rep.totalfees.week),
|
||||||
|
xfmt.umetric(rep.totalfees.month),
|
||||||
|
});
|
||||||
|
|
||||||
|
// channels section
|
||||||
|
tab.channels_cont.deleteChildren();
|
||||||
|
for (rep.channels) |ch| {
|
||||||
|
const chbox = (try lvgl.Container.new(tab.channels_cont)).flex(.column, .{});
|
||||||
|
chbox.setWidth(lvgl.sizePercent(100));
|
||||||
|
chbox.setHeightToContent();
|
||||||
|
_ = try switch (ch.state) {
|
||||||
|
// TODO: sanitize peer_alias?
|
||||||
|
.active => lvgl.Label.newFmt(chbox, &buf, "{s}", .{ch.peer_alias}, .{}),
|
||||||
|
.inactive => lvgl.Label.newFmt(chbox, &buf, "#ff0000 [INACTIVE]# {s}", .{ch.peer_alias}, .{ .recolor = true }),
|
||||||
|
.pending_open => lvgl.Label.new(chbox, "#00ff00 [PENDING OPEN]#", .{ .recolor = true }),
|
||||||
|
.pending_close => lvgl.Label.new(chbox, "#ffff00 [PENDING CLOSE]#", .{ .recolor = true }),
|
||||||
|
};
|
||||||
|
const row = try lvgl.FlexLayout.new(chbox, .row, .{});
|
||||||
|
row.setWidth(lvgl.sizePercent(100));
|
||||||
|
row.clearFlag(.scrollable);
|
||||||
|
row.setHeightToContent();
|
||||||
|
|
||||||
|
// left column
|
||||||
|
const left = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
|
left.setWidth(lvgl.sizePercent(46));
|
||||||
|
left.setHeightToContent();
|
||||||
|
left.setPad(10, .row, .{});
|
||||||
|
const bbar = try lvgl.Bar.new(left);
|
||||||
|
bbar.setWidth(lvgl.sizePercent(100));
|
||||||
|
const chan_local_pct: i32 = pct: {
|
||||||
|
const total = ch.balance.local + ch.balance.remote;
|
||||||
|
if (total == 0) {
|
||||||
|
break :pct 0;
|
||||||
|
}
|
||||||
|
const v = @as(f64, @floatFromInt(ch.balance.local)) / @as(f64, @floatFromInt(total));
|
||||||
|
break :pct @intFromFloat(v * 100);
|
||||||
|
};
|
||||||
|
bbar.setValue(chan_local_pct);
|
||||||
|
const subrow = try lvgl.FlexLayout.new(left, .row, .{ .main = .space_between });
|
||||||
|
subrow.setWidth(lvgl.sizePercent(100));
|
||||||
|
subrow.setHeightToContent();
|
||||||
|
const subcol1 = try lvgl.FlexLayout.new(subrow, .column, .{});
|
||||||
|
subcol1.setPad(10, .row, .{});
|
||||||
|
subcol1.setHeightToContent();
|
||||||
|
const subcol2 = try lvgl.FlexLayout.new(subrow, .column, .{});
|
||||||
|
subcol2.setPad(10, .row, .{});
|
||||||
|
_ = try lvgl.Label.newFmt(subcol1, &buf, cmark ++ "LOCAL#\n{} sat", .{xfmt.imetric(ch.balance.local)}, .{ .recolor = true });
|
||||||
|
_ = try lvgl.Label.newFmt(subcol1, &buf, cmark ++ "RECEIVED#\n{} sat", .{xfmt.imetric(ch.totalsats.received)}, .{ .recolor = true });
|
||||||
|
if (ch.state == .active or ch.state == .inactive) {
|
||||||
|
_ = try lvgl.Label.newFmt(subcol1, &buf, cmark ++ "BASE FEE#\n{} msat", .{xfmt.imetric(ch.fees.base)}, .{ .recolor = true });
|
||||||
|
_ = try lvgl.Label.newFmt(subcol1, &buf, cmark ++ "FEE PPM#\n{d}", .{ch.fees.ppm}, .{ .recolor = true });
|
||||||
|
}
|
||||||
|
_ = try lvgl.Label.newFmt(subcol2, &buf, cmark ++ "REMOTE#\n{} sat", .{xfmt.imetric(ch.balance.remote)}, .{ .recolor = true });
|
||||||
|
_ = try lvgl.Label.newFmt(subcol2, &buf, cmark ++ "SENT#\n{} sat", .{xfmt.imetric(ch.totalsats.sent)}, .{ .recolor = true });
|
||||||
|
|
||||||
|
// right column
|
||||||
|
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
|
right.setWidth(lvgl.sizePercent(54));
|
||||||
|
right.setHeightToContent();
|
||||||
|
right.setPad(10, .row, .{});
|
||||||
|
if (ch.id) |id| {
|
||||||
|
_ = try lvgl.Label.newFmt(right, &buf, cmark ++ "ID#\n{s}", .{id}, .{ .recolor = true });
|
||||||
|
}
|
||||||
|
_ = try lvgl.Label.newFmt(right, &buf, cmark ++ "FUNDING TX#\n{s}\n{s}", .{ ch.point[0..32], ch.point[32..] }, .{ .recolor = true });
|
||||||
|
if (ch.closetxid) |tx| {
|
||||||
|
_ = try lvgl.Label.newFmt(right, &buf, cmark ++ "CLOSING TX#\n{s}\n{s}", .{ tx[0..32], tx[32..] }, .{ .recolor = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue