@ -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