nd,ngui: add lightning setup process, phone pairing and reset #31
75
src/comm.zig
75
src/comm.zig
|
@ -69,11 +69,25 @@ pub const MessageTag = enum(u16) {
|
|||
onchain_report = 0x0a,
|
||||
// nd -> ngui: lnd status and stats report
|
||||
lightning_report = 0x0b,
|
||||
// nd -> ngui: error report when not in a regular running mode
|
||||
lightning_error = 0x0e,
|
||||
// ngui -> nd: call lnd to generate a new seed during initial setup
|
||||
lightning_genseed = 0x0f,
|
||||
// nd -> ngui: the result of genseed
|
||||
lightning_genseed_result = 0x10,
|
||||
// ngui -> nd: proceed with initializing a new or existing wallet
|
||||
lightning_init_wallet = 0x11,
|
||||
// ngui -> nd: request connection URLs for a controller app
|
||||
lightning_get_ctrlconn = 0x12,
|
||||
// nd -> ngui: lightning_get_ctrlconn result
|
||||
lightning_ctrlconn = 0x13,
|
||||
// ngui -> nd: factory reset lnd node; wipes out the wallet
|
||||
lightning_reset = 0x14,
|
||||
// ngui -> nd: switch sysupdates channel
|
||||
switch_sysupdates = 0x0c,
|
||||
// nd -> ngui: all ndg settings
|
||||
settings = 0x0d,
|
||||
// next: 0x0e
|
||||
// next: 0x15
|
||||
};
|
||||
|
||||
/// daemon and gui exchange messages of this type.
|
||||
|
@ -89,6 +103,13 @@ pub const Message = union(MessageTag) {
|
|||
poweroff_progress: PoweroffProgress,
|
||||
onchain_report: OnchainReport,
|
||||
lightning_report: LightningReport,
|
||||
lightning_error: LightningError,
|
||||
lightning_genseed: LightningGenSeed,
|
||||
lightning_genseed_result: []const []const u8,
|
||||
lightning_init_wallet: LightningInitWallet,
|
||||
lightning_get_ctrlconn: void,
|
||||
lightning_ctrlconn: LightningCtrlConn,
|
||||
lightning_reset: void,
|
||||
switch_sysupdates: SysupdatesChan,
|
||||
settings: Settings,
|
||||
|
||||
|
@ -188,6 +209,38 @@ pub const Message = union(MessageTag) {
|
|||
},
|
||||
};
|
||||
|
||||
pub const LightningCtrlConn = []const LnCtrlConnItem;
|
||||
|
||||
pub const LnCtrlConnItem = struct {
|
||||
url: []const u8,
|
||||
typ: enum { lnd_rpc, lnd_http },
|
||||
perm: enum { admin }, // TODO: support read-only and invoice-only permissions
|
||||
};
|
||||
|
||||
pub const LightningError = struct {
|
||||
code: enum(u8) {
|
||||
uninitialized, // wallet uninitialized
|
||||
//init_failed, TODO: when .lightning_init_wallet results in an error
|
||||
not_ready, // in a startup mode
|
||||
locked, // wallet locked
|
||||
},
|
||||
};
|
||||
|
||||
/// https://lightning.engineering/api-docs/api/lnd/wallet-unlocker/gen-seed
|
||||
pub const LightningGenSeed = struct {
|
||||
// TODO: support passphrase
|
||||
//passphrase: ?[]const u8,
|
||||
};
|
||||
|
||||
/// https://lightning.engineering/api-docs/api/lnd/wallet-unlocker/init-wallet
|
||||
pub const LightningInitWallet = struct {
|
||||
mnemonic: []const []const u8, // 24 words
|
||||
// TODO: support passphrase
|
||||
//passphrase: ?[]const u8,
|
||||
|
||||
// TODO: support extra fields for restoring an existing wallet, like recovery_window
|
||||
};
|
||||
|
||||
pub const SysupdatesChan = enum {
|
||||
stable, // master branch in sysupdates
|
||||
edge, // dev branch in sysupdates
|
||||
|
@ -226,6 +279,8 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
|
|||
const len = try reader.readIntLittle(u64);
|
||||
if (len == 0) {
|
||||
return switch (tag) {
|
||||
.lightning_get_ctrlconn => .{ .value = .lightning_get_ctrlconn },
|
||||
.lightning_reset => .{ .value = .lightning_reset },
|
||||
.ping => .{ .value = .{ .ping = {} } },
|
||||
.pong => .{ .value = .{ .pong = {} } },
|
||||
.poweroff => .{ .value = .{ .poweroff = {} } },
|
||||
|
@ -235,7 +290,14 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
|
|||
};
|
||||
}
|
||||
switch (tag) {
|
||||
.ping, .pong, .poweroff, .standby, .wakeup => unreachable, // handled above
|
||||
.lightning_get_ctrlconn,
|
||||
.lightning_reset,
|
||||
.ping,
|
||||
.pong,
|
||||
.poweroff,
|
||||
.standby,
|
||||
.wakeup,
|
||||
=> unreachable, // handled above
|
||||
inline else => |t| {
|
||||
var bytes = try allocator.alloc(u8, len);
|
||||
defer allocator.free(bytes);
|
||||
|
@ -271,6 +333,13 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
|
|||
.poweroff_progress => try json.stringify(msg.poweroff_progress, .{}, data.writer()),
|
||||
.onchain_report => try json.stringify(msg.onchain_report, .{}, data.writer()),
|
||||
.lightning_report => try json.stringify(msg.lightning_report, .{}, data.writer()),
|
||||
.lightning_error => try json.stringify(msg.lightning_error, .{}, data.writer()),
|
||||
.lightning_genseed => try json.stringify(msg.lightning_genseed, .{}, data.writer()),
|
||||
.lightning_genseed_result => try json.stringify(msg.lightning_genseed_result, .{}, data.writer()),
|
||||
.lightning_init_wallet => try json.stringify(msg.lightning_init_wallet, .{}, data.writer()),
|
||||
.lightning_get_ctrlconn => {}, // zero length payload
|
||||
.lightning_ctrlconn => try json.stringify(msg.lightning_ctrlconn, .{}, data.writer()),
|
||||
.lightning_reset => {}, // zero length payload
|
||||
.switch_sysupdates => try json.stringify(msg.switch_sysupdates, .{}, data.writer()),
|
||||
.settings => try json.stringify(msg.settings, .{}, data.writer()),
|
||||
}
|
||||
|
@ -364,6 +433,8 @@ test "write/read void tags" {
|
|||
defer buf.deinit();
|
||||
|
||||
const msg = [_]Message{
|
||||
Message.lightning_get_ctrlconn,
|
||||
Message.lightning_reset,
|
||||
Message.ping,
|
||||
Message.pong,
|
||||
Message.poweroff,
|
||||
|
|
154
src/lndhttp.zig
154
src/lndhttp.zig
|
@ -1,6 +1,8 @@
|
|||
//! lnd lightning HTTP client and utility functions.
|
||||
|
||||
const std = @import("std");
|
||||
const base64enc = std.base64.standard.Encoder;
|
||||
|
||||
const types = @import("types.zig");
|
||||
|
||||
/// safe for concurrent use as long as Client.allocator is.
|
||||
|
@ -10,30 +12,43 @@ pub const Client = struct {
|
|||
port: u16 = 10010,
|
||||
apibase: []const u8, // https://localhost:10010
|
||||
macaroon: struct {
|
||||
readonly: []const u8,
|
||||
readonly: ?[]const u8,
|
||||
admin: ?[]const u8,
|
||||
},
|
||||
httpClient: std.http.Client,
|
||||
|
||||
pub const Error = error{
|
||||
LndHttpMissingMacaroon,
|
||||
LndHttpBadStatusCode,
|
||||
LndPayloadWriteFail,
|
||||
};
|
||||
|
||||
pub const ApiMethod = enum {
|
||||
// no auth methods
|
||||
genseed, // generate a new wallet seed; non-committing
|
||||
walletstatus, // server/wallet status
|
||||
initwallet, // commit a seed and create a node wallet
|
||||
unlockwallet, // required after successfull initwallet
|
||||
// read-only
|
||||
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",
|
||||
.genseed => "v1/genseed",
|
||||
.getinfo => "v1/getinfo",
|
||||
.getnetworkinfo => "v1/graph/info",
|
||||
.initwallet => "v1/initwallet",
|
||||
.listchannels => "v1/channels",
|
||||
.pendingchannels => "v1/channels/pending",
|
||||
.unlockwallet => "v1/unlockwallet",
|
||||
.walletbalance => "v1/balance/blockchain",
|
||||
.walletstatus => "v1/state",
|
||||
};
|
||||
|
@ -42,6 +57,20 @@ pub const Client = struct {
|
|||
|
||||
pub fn MethodArgs(comptime m: ApiMethod) type {
|
||||
return switch (m) {
|
||||
.initwallet => struct {
|
||||
unlock_password: []const u8, // min 8 bytes
|
||||
mnemonic: []const []const u8, // 24 words
|
||||
passphrase: ?[]const u8 = null,
|
||||
// TODO: restore an existing wallet:
|
||||
//recovery_window: i32 = 0, // applies to each branch of BIP44 derivation path
|
||||
//channel_backups
|
||||
},
|
||||
.unlockwallet => struct {
|
||||
unlock_password: []const u8, // from initwallet
|
||||
// TODO: restore an existing wallet:
|
||||
//recovery_window: i32 = 0, // applies to each branch of BIP44 derivation path
|
||||
//channel_backups
|
||||
},
|
||||
.listchannels => struct {
|
||||
status: ?enum { active, inactive } = null,
|
||||
advert: ?enum { public, private } = null,
|
||||
|
@ -55,10 +84,13 @@ pub const Client = struct {
|
|||
pub fn ResultValue(comptime m: ApiMethod) type {
|
||||
return switch (m) {
|
||||
.feereport => FeeReport,
|
||||
.genseed => GeneratedSeed,
|
||||
.getinfo => LndInfo,
|
||||
.getnetworkinfo => NetworkInfo,
|
||||
.initwallet => InitedWallet,
|
||||
.listchannels => ChannelsList,
|
||||
.pendingchannels => PendingList,
|
||||
.unlockwallet => struct {},
|
||||
.walletbalance => WalletBalance,
|
||||
.walletstatus => WalletStatus,
|
||||
};
|
||||
|
@ -69,7 +101,7 @@ pub const Client = struct {
|
|||
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_ro_path: ?[]const u8 = null, // readonly macaroon path
|
||||
macaroon_admin_path: ?[]const u8 = null, // required only for requests mutating lnd state
|
||||
};
|
||||
|
||||
|
@ -77,13 +109,14 @@ pub const Client = struct {
|
|||
/// 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);
|
||||
errdefer ca.deinit(opt.allocator);
|
||||
const mac_ro: ?[]const u8 = if (opt.macaroon_ro_path) |p| try readMacaroonOrNull(opt.allocator, p) else null;
|
||||
errdefer if (mac_ro) |v| opt.allocator.free(v);
|
||||
const mac_admin: ?[]const u8 = if (opt.macaroon_admin_path) |p| try readMacaroonOrNull(opt.allocator, p) else null;
|
||||
errdefer if (mac_admin) |v| opt.allocator.free(v);
|
||||
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,
|
||||
|
@ -99,10 +132,8 @@ pub const Client = struct {
|
|||
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);
|
||||
}
|
||||
if (self.macaroon.readonly) |ro| self.allocator.free(ro);
|
||||
if (self.macaroon.admin) |a| self.allocator.free(a);
|
||||
}
|
||||
|
||||
pub fn Result(comptime m: ApiMethod) type {
|
||||
|
@ -116,14 +147,21 @@ pub const Client = struct {
|
|||
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();
|
||||
if (reqinfo.payload) |p| {
|
||||
req.transfer_encoding = .{ .content_length = p.len };
|
||||
}
|
||||
|
||||
try req.start();
|
||||
if (reqinfo.payload) |p| {
|
||||
try req.writer().writeAll(p);
|
||||
req.writer().writeAll(p) catch return Error.LndPayloadWriteFail;
|
||||
try req.finish();
|
||||
}
|
||||
try req.wait();
|
||||
if (req.response.status.class() != .success) {
|
||||
return error.LndHttpBadStatusCode;
|
||||
// a structured error reporting in lnd is in a less than desirable state.
|
||||
// https://github.com/lightningnetwork/lnd/issues/5586
|
||||
// TODO: return a more detailed error when the upstream improves.
|
||||
return Error.LndHttpBadStatusCode;
|
||||
}
|
||||
if (@TypeOf(Result(apimethod)) == void) {
|
||||
return; // void response; need no json parsing
|
||||
|
@ -153,12 +191,61 @@ pub const Client = struct {
|
|||
errdefer reqinfo.deinit();
|
||||
const arena = reqinfo.arena.allocator();
|
||||
reqinfo.value = switch (apimethod) {
|
||||
.feereport, .getinfo, .getnetworkinfo, .pendingchannels, .walletbalance, .walletstatus => |m| .{
|
||||
.genseed, .walletstatus => |m| .{
|
||||
.httpmethod = .GET,
|
||||
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
||||
.headers = std.http.Headers{ .allocator = arena },
|
||||
.payload = null,
|
||||
},
|
||||
.initwallet => |m| blk: {
|
||||
const payload = p: {
|
||||
var params: struct {
|
||||
wallet_password: []const u8, // base64
|
||||
cipher_seed_mnemonic: []const []const u8,
|
||||
aezeed_passphrase: ?[]const u8 = null, // base64
|
||||
} = .{
|
||||
.wallet_password = try base64EncodeAlloc(arena, args.unlock_password),
|
||||
.cipher_seed_mnemonic = args.mnemonic,
|
||||
.aezeed_passphrase = if (args.passphrase) |p| try base64EncodeAlloc(arena, p) else null,
|
||||
};
|
||||
var buf = std.ArrayList(u8).init(arena);
|
||||
try std.json.stringify(params, .{ .emit_null_optional_fields = false }, buf.writer());
|
||||
break :p try buf.toOwnedSlice();
|
||||
};
|
||||
break :blk .{
|
||||
.httpmethod = .POST,
|
||||
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
||||
.headers = std.http.Headers{ .allocator = arena },
|
||||
.payload = payload,
|
||||
};
|
||||
},
|
||||
.unlockwallet => |m| blk: {
|
||||
const payload = p: {
|
||||
var params: struct {
|
||||
wallet_password: []const u8, // base64
|
||||
} = .{
|
||||
.wallet_password = try base64EncodeAlloc(arena, args.unlock_password),
|
||||
};
|
||||
var buf = std.ArrayList(u8).init(arena);
|
||||
try std.json.stringify(params, .{ .emit_null_optional_fields = false }, buf.writer());
|
||||
break :p try buf.toOwnedSlice();
|
||||
};
|
||||
break :blk .{
|
||||
.httpmethod = .POST,
|
||||
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
||||
.headers = std.http.Headers{ .allocator = arena },
|
||||
.payload = payload,
|
||||
};
|
||||
},
|
||||
.feereport, .getinfo, .getnetworkinfo, .pendingchannels, .walletbalance => |m| .{
|
||||
.httpmethod = .GET,
|
||||
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
||||
.headers = blk: {
|
||||
if (self.macaroon.readonly == null) {
|
||||
return Error.LndHttpMissingMacaroon;
|
||||
}
|
||||
var h = std.http.Headers{ .allocator = arena };
|
||||
try h.append(authHeaderName, self.macaroon.readonly);
|
||||
try h.append(authHeaderName, self.macaroon.readonly.?);
|
||||
break :blk h;
|
||||
},
|
||||
.payload = null,
|
||||
|
@ -184,8 +271,11 @@ pub const Client = struct {
|
|||
break :blk try std.Uri.parse(buf.items); // uri point to the original buf
|
||||
},
|
||||
.headers = blk: {
|
||||
if (self.macaroon.readonly == null) {
|
||||
return Error.LndHttpMissingMacaroon;
|
||||
}
|
||||
var h = std.http.Headers{ .allocator = arena };
|
||||
try h.append(authHeaderName, self.macaroon.readonly);
|
||||
try h.append(authHeaderName, self.macaroon.readonly.?);
|
||||
break :blk h;
|
||||
},
|
||||
.payload = null,
|
||||
|
@ -194,13 +284,23 @@ pub const Client = struct {
|
|||
return reqinfo;
|
||||
}
|
||||
|
||||
/// returns null if file not found.
|
||||
/// 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 });
|
||||
fn readMacaroonOrNull(gpa: std.mem.Allocator, path: []const u8) !?[]const u8 {
|
||||
const file = std.fs.openFileAbsolute(path, .{ .mode = .read_only }) catch |err| switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
const cont = try file.readToEndAlloc(gpa, 1024);
|
||||
defer gpa.free(cont);
|
||||
return std.fmt.allocPrint(gpa, "{}", .{std.fmt.fmtSliceHexLower(cont)});
|
||||
const raw = try file.readToEndAlloc(gpa, 1024);
|
||||
defer gpa.free(raw);
|
||||
const hex = try std.fmt.allocPrint(gpa, "{}", .{std.fmt.fmtSliceHexLower(raw)});
|
||||
return hex;
|
||||
}
|
||||
|
||||
fn base64EncodeAlloc(gpa: std.mem.Allocator, v: []const u8) ![]const u8 {
|
||||
var buf = try gpa.alloc(u8, base64enc.calcSize(v.len));
|
||||
return base64enc.encode(buf, v); // always returns a slice of buf.len
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -326,3 +426,13 @@ pub const WalletStatus = struct {
|
|||
WAITING_TO_START = 255,
|
||||
},
|
||||
};
|
||||
|
||||
/// https://lightning.engineering/api-docs/api/lnd/wallet-unlocker/gen-seed
|
||||
pub const GeneratedSeed = struct {
|
||||
cipher_seed_mnemonic: []const []const u8, // 24 words aezeed
|
||||
};
|
||||
|
||||
/// https://lightning.engineering/api-docs/api/lnd/wallet-unlocker/init-wallet
|
||||
pub const InitedWallet = struct {
|
||||
admin_macaroon: []const u8, // base64?
|
||||
};
|
||||
|
|
|
@ -11,9 +11,25 @@ const SYSUPDATES_CRON_SCRIPT_PATH = "/etc/cron.hourly/sysupdate";
|
|||
const SYSUPDATES_RUN_SCRIPT_NAME = "update.sh";
|
||||
const SYSUPDATES_RUN_SCRIPT_PATH = "/ssd/sysupdates/" ++ SYSUPDATES_RUN_SCRIPT_NAME;
|
||||
|
||||
/// must be the same as https://git.qcode.ch/nakamochi/sysupdates/src/branch/master/lnd
|
||||
pub const LND_OS_USER = "lnd";
|
||||
pub const LND_DATA_DIR = "/ssd/lnd/data";
|
||||
pub const LND_LOG_DIR = "/ssd/lnd/logs";
|
||||
pub const LND_HOMEDIR = "/home/lnd";
|
||||
pub const LND_CONF_PATH = LND_HOMEDIR ++ "/lnd.mainnet.conf";
|
||||
pub const LND_TLSKEY_PATH = LND_HOMEDIR ++ "/.lnd/tls.key";
|
||||
pub const LND_TLSCERT_PATH = LND_HOMEDIR ++ "/.lnd/tls.cert";
|
||||
pub const LND_WALLETUNLOCK_PATH = LND_HOMEDIR ++ "/walletunlock.txt";
|
||||
pub const LND_MACAROON_RO_PATH = LND_DATA_DIR ++ "/chain/bitcoin/mainnet/readonly.macaroon";
|
||||
pub const LND_MACAROON_ADMIN_PATH = LND_DATA_DIR ++ "/chain/bitcoin/mainnet/admin.macaroon";
|
||||
|
||||
pub const BITCOIND_CONFIG_PATH = "/home/bitcoind/mainnet.conf";
|
||||
pub const TOR_DATA_DIR = "/ssd/tor";
|
||||
|
||||
arena: *std.heap.ArenaAllocator, // data is allocated here
|
||||
confpath: []const u8, // fs path to where data is persisted
|
||||
|
||||
static: StaticData,
|
||||
mu: std.Thread.RwLock = .{},
|
||||
data: Data,
|
||||
|
||||
|
@ -25,6 +41,12 @@ pub const Data = struct {
|
|||
sysrunscript: []const u8,
|
||||
};
|
||||
|
||||
/// static data is always interred at init and never changes.
|
||||
pub const StaticData = struct {
|
||||
lnd_tor_hostname: ?[]const u8,
|
||||
bitcoind_rpc_pass: ?[]const u8,
|
||||
};
|
||||
|
||||
/// enums must match git branches in https://git.qcode.ch/nakamochi/sysupdates.
|
||||
pub const SysupdatesChannel = enum {
|
||||
master, // stable
|
||||
|
@ -43,8 +65,9 @@ pub fn init(allocator: std.mem.Allocator, confpath: []const u8) !Config {
|
|||
}
|
||||
return .{
|
||||
.arena = arena,
|
||||
.data = try initData(arena.allocator(), confpath),
|
||||
.confpath = confpath,
|
||||
.data = try initData(arena.allocator(), confpath),
|
||||
.static = inferStaticData(arena.allocator()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -92,6 +115,42 @@ fn inferSysupdatesChannel(cron_script_path: []const u8) SysupdatesChannel {
|
|||
return .master;
|
||||
}
|
||||
|
||||
fn inferStaticData(allocator: std.mem.Allocator) StaticData {
|
||||
return .{
|
||||
.lnd_tor_hostname = inferLndTorHostname(allocator) catch null,
|
||||
.bitcoind_rpc_pass = inferBitcoindRpcPass(allocator) catch null,
|
||||
};
|
||||
}
|
||||
|
||||
fn inferLndTorHostname(allocator: std.mem.Allocator) ![]const u8 {
|
||||
var raw = try std.fs.cwd().readFileAlloc(allocator, TOR_DATA_DIR ++ "/lnd/hostname", 1024);
|
||||
const hostname = std.mem.trim(u8, raw, &std.ascii.whitespace);
|
||||
logger.info("inferred lnd tor hostname: [{s}]", .{hostname});
|
||||
return hostname;
|
||||
}
|
||||
|
||||
fn inferBitcoindRpcPass(allocator: std.mem.Allocator) ![]const u8 {
|
||||
// a hack to recover bitcoind rpc password from an original conf template.
|
||||
// the password was placed on a separate comment line, preceding another comment
|
||||
// line containing "rpcauth.py".
|
||||
// TODO: get rid of the hack; do something more robust
|
||||
var conf = try std.fs.cwd().readFileAlloc(allocator, BITCOIND_CONFIG_PATH, 1024 * 1024);
|
||||
var it = std.mem.tokenizeScalar(u8, conf, '\n');
|
||||
var next_is_pass = false;
|
||||
while (it.next()) |line| {
|
||||
if (next_is_pass) {
|
||||
if (!std.mem.startsWith(u8, line, "#")) {
|
||||
return error.UninferrableBitcoindRpcPass;
|
||||
}
|
||||
return std.mem.trim(u8, line[1..], &std.ascii.whitespace);
|
||||
}
|
||||
if (std.mem.startsWith(u8, line, "#") and std.mem.indexOf(u8, line, "rpcauth.py") != null) {
|
||||
next_is_pass = true;
|
||||
}
|
||||
}
|
||||
return error.UninferrableBitcoindRpcPass;
|
||||
}
|
||||
|
||||
/// calls F while holding a readonly lock and passes on F's result as is.
|
||||
pub fn safeReadOnly(self: *Config, comptime F: anytype) @typeInfo(@TypeOf(F)).Fn.return_type.? {
|
||||
self.mu.lockShared();
|
||||
|
@ -171,6 +230,131 @@ fn runSysupdates(allocator: std.mem.Allocator, scriptpath: []const u8) !void {
|
|||
}
|
||||
}
|
||||
|
||||
/// waits until lnd admin macaroon is readable and returns an lndconnect URL.
|
||||
/// caller owns returned value.
|
||||
pub fn lndConnectWaitMacaroonFile(self: Config, allocator: std.mem.Allocator, typ: enum { tor_rpc, tor_http }) ![]const u8 {
|
||||
var macaroon: []const u8 = undefined;
|
||||
while (true) {
|
||||
macaroon = std.fs.cwd().readFileAlloc(allocator, LND_MACAROON_ADMIN_PATH, 2048) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.atomic.spinLoopHint();
|
||||
std.time.sleep(1 * std.time.ns_per_s);
|
||||
continue;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
defer allocator.free(macaroon);
|
||||
|
||||
const base64enc = std.base64.url_safe_no_pad.Encoder;
|
||||
var buf = try allocator.alloc(u8, base64enc.calcSize(macaroon.len));
|
||||
defer allocator.free(buf);
|
||||
const macaroon_b64 = base64enc.encode(buf, macaroon);
|
||||
const port: u16 = switch (typ) {
|
||||
.tor_rpc => 10009,
|
||||
.tor_http => 10010,
|
||||
};
|
||||
return std.fmt.allocPrint(allocator, "lndconnect://{[host]s}:{[port]d}?macaroon={[macaroon]s}", .{
|
||||
.host = self.static.lnd_tor_hostname orelse "<no-tor-hostname>.onion",
|
||||
.port = port,
|
||||
.macaroon = macaroon_b64,
|
||||
});
|
||||
}
|
||||
|
||||
/// generates a random bytes sequence of the given size, dumps it into `LND_WALLETUNLOCK_PATH`
|
||||
/// file, changing the ownership to `LND_OS_USER`, as well as into the buf in hex encoding.
|
||||
/// the buffer must be at least twice the size.
|
||||
/// returns the bytes printed to outbuf.
|
||||
pub fn makeWalletUnlockFile(self: Config, outbuf: []u8, comptime raw_size: usize) ![]const u8 {
|
||||
const filepath = LND_WALLETUNLOCK_PATH;
|
||||
const lnduser = try std.process.getUserInfo(LND_OS_USER);
|
||||
|
||||
const allocator = self.arena.child_allocator;
|
||||
const opt = .{ .mode = 0o400 };
|
||||
const file = try std.io.BufferedAtomicFile.create(allocator, std.fs.cwd(), filepath, opt);
|
||||
defer file.destroy(); // frees resources; does NOT delete the file
|
||||
|
||||
var raw_unlock_pwd: [raw_size]u8 = undefined;
|
||||
std.crypto.random.bytes(&raw_unlock_pwd);
|
||||
const hex = try std.fmt.bufPrint(outbuf, "{}", .{std.fmt.fmtSliceHexLower(&raw_unlock_pwd)});
|
||||
try file.writer().writeAll(hex);
|
||||
try file.finish();
|
||||
|
||||
const f = try std.fs.cwd().openFile(filepath, .{});
|
||||
defer f.close();
|
||||
try f.chown(lnduser.uid, lnduser.gid);
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
/// creates or overwrites existing lnd config file at `LND_CONF_PATH`.
|
||||
pub fn genLndConfig(self: Config, opt: struct { autounlock: bool }) !void {
|
||||
const confpath = LND_CONF_PATH;
|
||||
const lnduser = try std.process.getUserInfo(LND_OS_USER);
|
||||
|
||||
const allocator = self.arena.child_allocator;
|
||||
const file = try std.io.BufferedAtomicFile.create(allocator, std.fs.cwd(), confpath, .{ .mode = 0o400 });
|
||||
defer file.destroy(); // frees resources; does NOT delete the file
|
||||
const w = file.writer();
|
||||
|
||||
// main app settings
|
||||
try w.writeAll("[Application Options]\n");
|
||||
try w.writeAll("debuglevel=info\n");
|
||||
try w.writeAll("maxpendingchannels=10\n");
|
||||
try w.writeAll("maxlogfiles=3\n");
|
||||
try w.writeAll("listen=[::]:9735\n"); // or 0.0.0.0:9735
|
||||
try w.writeAll("rpclisten=0.0.0.0:10009\n");
|
||||
try w.writeAll("restlisten=0.0.0.0:10010\n"); // TODO: replace with 127.0.0.1 and no-rest-tls=true?
|
||||
try std.fmt.format(w, "alias={s}\n", .{"nakamochi"}); // TODO: make alias configurable
|
||||
try std.fmt.format(w, "datadir={s}\n", .{LND_DATA_DIR});
|
||||
try std.fmt.format(w, "logdir={s}\n", .{LND_LOG_DIR});
|
||||
if (self.static.lnd_tor_hostname) |torhost| {
|
||||
try std.fmt.format(w, "tlsextradomain={s}\n", .{torhost});
|
||||
try std.fmt.format(w, "externalhosts={s}\n", .{torhost});
|
||||
}
|
||||
if (opt.autounlock) {
|
||||
try std.fmt.format(w, "wallet-unlock-password-file={s}\n", .{LND_WALLETUNLOCK_PATH});
|
||||
}
|
||||
|
||||
// bitcoin chain settings
|
||||
try w.writeAll("\n[bitcoin]\n");
|
||||
try std.fmt.format(w, "bitcoin.chaindir={s}/chain/mainnet\n", .{LND_DATA_DIR});
|
||||
try w.writeAll("bitcoin.active=true\n");
|
||||
try w.writeAll("bitcoin.mainnet=True\n");
|
||||
try w.writeAll("bitcoin.testnet=False\n");
|
||||
try w.writeAll("bitcoin.regtest=False\n");
|
||||
try w.writeAll("bitcoin.simnet=False\n");
|
||||
try w.writeAll("bitcoin.node=bitcoind\n");
|
||||
try w.writeAll("\n[bitcoind]\n");
|
||||
try w.writeAll("bitcoind.zmqpubrawblock=tcp://127.0.0.1:8331\n");
|
||||
try w.writeAll("bitcoind.zmqpubrawtx=tcp://127.0.0.1:8330\n");
|
||||
try w.writeAll("bitcoind.rpchost=127.0.0.1\n");
|
||||
try w.writeAll("bitcoind.rpcuser=rpc\n");
|
||||
if (self.static.bitcoind_rpc_pass) |rpcpass| {
|
||||
try std.fmt.format(w, "bitcoind.rpcpass={s}\n", .{rpcpass});
|
||||
} else {
|
||||
return error.GenLndConfigNoBitcoindRpcPass;
|
||||
}
|
||||
|
||||
// other settings
|
||||
try w.writeAll("\n[autopilot]\n");
|
||||
try w.writeAll("autopilot.active=false\n");
|
||||
try w.writeAll("\n[tor]\n");
|
||||
try w.writeAll("tor.active=true\n");
|
||||
try w.writeAll("tor.skip-proxy-for-clearnet-targets=true\n");
|
||||
|
||||
// persist the file in the correct location.
|
||||
try file.finish();
|
||||
|
||||
// change file ownership to that of the lnd system user.
|
||||
const f = try std.fs.cwd().openFile(confpath, .{});
|
||||
defer f.close();
|
||||
try f.chown(lnduser.uid, lnduser.gid);
|
||||
}
|
||||
|
||||
test "init existing" {
|
||||
const t = std.testing;
|
||||
const tt = @import("../test.zig");
|
||||
|
@ -220,6 +404,7 @@ test "dump" {
|
|||
.syscronscript = "cronscript.sh",
|
||||
.sysrunscript = "runscript.sh",
|
||||
},
|
||||
.static = undefined,
|
||||
};
|
||||
// purposefully skip conf.deinit() - expecting no leaking allocations in conf.dump.
|
||||
try conf.dump();
|
||||
|
@ -252,6 +437,7 @@ test "switch sysupdates and infer" {
|
|||
.syscronscript = cronscript,
|
||||
.sysrunscript = SYSUPDATES_RUN_SCRIPT_PATH,
|
||||
},
|
||||
.static = undefined,
|
||||
};
|
||||
// purposefully skip conf.deinit() - expecting no leaking allocations.
|
||||
|
||||
|
@ -290,6 +476,7 @@ test "switch sysupdates with .run=true" {
|
|||
.syscronscript = try tmp.join(&.{"cronscript.sh"}),
|
||||
.sysrunscript = try tmp.join(&.{runscript}),
|
||||
},
|
||||
.static = undefined,
|
||||
};
|
||||
defer conf.deinit();
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ state: enum {
|
|||
running,
|
||||
standby,
|
||||
poweroff,
|
||||
wallet_reset,
|
||||
},
|
||||
|
||||
main_thread: ?std.Thread = null,
|
||||
|
@ -64,14 +65,56 @@ onchain_report_interval: u64 = 1 * time.ns_per_min,
|
|||
want_lnd_report: bool,
|
||||
lnd_timer: time.Timer,
|
||||
lnd_report_interval: u64 = 1 * time.ns_per_min,
|
||||
lnd_tls_reset_count: usize = 0,
|
||||
|
||||
/// system services actively managed by the daemon.
|
||||
/// these are stop'ed during poweroff and their shutdown progress sent to ngui.
|
||||
/// initialized in start and never modified again: ok to access without holding self.mu.
|
||||
services: []SysService = &.{},
|
||||
services: struct {
|
||||
list: []SysService,
|
||||
|
||||
fn stopWait(self: @This(), name: []const u8) !void {
|
||||
for (self.list) |*sv| {
|
||||
if (std.mem.eql(u8, sv.name, name)) {
|
||||
return sv.stopWait();
|
||||
}
|
||||
}
|
||||
return error.NoSuchServiceToStop;
|
||||
}
|
||||
|
||||
fn start(self: @This(), name: []const u8) !void {
|
||||
for (self.list) |*sv| {
|
||||
if (std.mem.eql(u8, sv.name, name)) {
|
||||
return sv.start();
|
||||
}
|
||||
}
|
||||
return error.NoSuchServiceToStart;
|
||||
}
|
||||
|
||||
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
|
||||
for (self.list) |*sv| {
|
||||
sv.deinit();
|
||||
}
|
||||
allocator.free(self.list);
|
||||
}
|
||||
} = .{ .list = &.{} },
|
||||
|
||||
const Daemon = @This();
|
||||
|
||||
const Error = error{
|
||||
InvalidState,
|
||||
WalletResetActive,
|
||||
PoweroffActive,
|
||||
AlreadyStarted,
|
||||
ConnectWifiEmptySSID,
|
||||
MakeWalletUnlockFileFail,
|
||||
LndServiceStopFail,
|
||||
ResetLndFail,
|
||||
GenLndConfigFail,
|
||||
InitLndWallet,
|
||||
UnlockLndWallet,
|
||||
};
|
||||
|
||||
const InitOpt = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
confpath: []const u8,
|
||||
|
@ -91,8 +134,8 @@ pub fn init(opt: InitOpt) !Daemon {
|
|||
}
|
||||
// the order is important. when powering off, the services are shut down
|
||||
// in the same order appended here.
|
||||
try svlist.append(SysService.init(opt.allocator, "lnd", .{ .stop_wait_sec = 600 }));
|
||||
try svlist.append(SysService.init(opt.allocator, "bitcoind", .{ .stop_wait_sec = 600 }));
|
||||
try svlist.append(SysService.init(opt.allocator, SysService.LND, .{ .stop_wait_sec = 600 }));
|
||||
try svlist.append(SysService.init(opt.allocator, SysService.BITCOIND, .{ .stop_wait_sec = 600 }));
|
||||
|
||||
const conf = try Config.init(opt.allocator, opt.confpath);
|
||||
errdefer conf.deinit();
|
||||
|
@ -103,7 +146,7 @@ pub fn init(opt: InitOpt) !Daemon {
|
|||
.uiwriter = opt.uiw,
|
||||
.wpa_ctrl = try types.WpaControl.open(opt.wpa),
|
||||
.state = .stopped,
|
||||
.services = try svlist.toOwnedSlice(),
|
||||
.services = .{ .list = try svlist.toOwnedSlice() },
|
||||
// send persisted settings immediately on start
|
||||
.want_settings = true,
|
||||
// send a network report right at start without wifi scan to make it faster.
|
||||
|
@ -122,12 +165,9 @@ pub fn init(opt: InitOpt) !Daemon {
|
|||
/// releases all associated resources.
|
||||
/// the daemon must be stop'ed and wait'ed before deiniting.
|
||||
pub fn deinit(self: *Daemon) void {
|
||||
defer self.conf.deinit();
|
||||
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
|
||||
for (self.services) |*sv| {
|
||||
sv.deinit();
|
||||
}
|
||||
self.allocator.free(self.services);
|
||||
self.services.deinit(self.allocator);
|
||||
self.conf.deinit();
|
||||
}
|
||||
|
||||
/// start launches daemon threads and returns immediately.
|
||||
|
@ -138,8 +178,8 @@ pub fn start(self: *Daemon) !void {
|
|||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.stopped => {}, // continue
|
||||
.poweroff => return error.InPoweroffState,
|
||||
else => return error.AlreadyStarted,
|
||||
.poweroff => return Error.PoweroffActive,
|
||||
else => return Error.AlreadyStarted,
|
||||
}
|
||||
|
||||
try self.wpa_ctrl.attach();
|
||||
|
@ -193,7 +233,8 @@ fn standby(self: *Daemon) !void {
|
|||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.standby => {},
|
||||
.stopped, .poweroff => return error.InvalidState,
|
||||
.stopped, .poweroff => return Error.InvalidState,
|
||||
.wallet_reset => return Error.WalletResetActive,
|
||||
.running => {
|
||||
try screen.backlight(.off);
|
||||
self.state = .standby;
|
||||
|
@ -206,8 +247,8 @@ fn wakeup(self: *Daemon) !void {
|
|||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.running => {},
|
||||
.stopped, .poweroff => return error.InvalidState,
|
||||
.running, .wallet_reset => {},
|
||||
.stopped, .poweroff => return Error.InvalidState,
|
||||
.standby => {
|
||||
try screen.backlight(.on);
|
||||
self.state = .running;
|
||||
|
@ -225,7 +266,8 @@ fn beginPoweroff(self: *Daemon) !void {
|
|||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.poweroff => {}, // already in poweroff mode
|
||||
.stopped => return error.InvalidState,
|
||||
.stopped => return Error.InvalidState,
|
||||
.wallet_reset => return Error.WalletResetActive,
|
||||
.running, .standby => {
|
||||
self.poweroff_thread = try std.Thread.spawn(.{}, poweroffThread, .{self});
|
||||
self.state = .poweroff;
|
||||
|
@ -250,13 +292,13 @@ fn poweroffThread(self: *Daemon) void {
|
|||
};
|
||||
|
||||
// initiate shutdown of all services concurrently.
|
||||
for (self.services) |*sv| {
|
||||
for (self.services.list) |*sv| {
|
||||
sv.stop() catch |err| logger.err("sv stop '{s}': {any}", .{ sv.name, err });
|
||||
}
|
||||
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
||||
|
||||
// wait each service until stopped or error.
|
||||
for (self.services) |*sv| {
|
||||
for (self.services.list) |*sv| {
|
||||
_ = sv.stopWait() catch {};
|
||||
logger.info("{s} sv is now stopped; err={any}", .{ sv.name, sv.lastStopError() });
|
||||
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
||||
|
@ -311,6 +353,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
|
|||
self.want_settings = !ok;
|
||||
}
|
||||
|
||||
// network stats
|
||||
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
||||
if (self.want_wifi_scan) {
|
||||
if (self.startWifiScan()) {
|
||||
|
@ -327,6 +370,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
|
|||
}
|
||||
}
|
||||
|
||||
// onchain bitcoin stats
|
||||
if (self.want_onchain_report or self.bitcoin_timer.read() > self.onchain_report_interval) {
|
||||
if (self.sendOnchainReport()) {
|
||||
self.bitcoin_timer.reset();
|
||||
|
@ -335,12 +379,17 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
|
|||
logger.err("sendOnchainReport: {any}", .{err});
|
||||
}
|
||||
}
|
||||
|
||||
// lightning stats
|
||||
if (self.state != .wallet_reset) {
|
||||
if (self.want_lnd_report or self.lnd_timer.read() > self.lnd_report_interval) {
|
||||
if (self.sendLightningReport()) {
|
||||
self.lnd_timer.reset();
|
||||
self.want_lnd_report = false;
|
||||
} else |err| {
|
||||
logger.err("sendLightningReport: {any}", .{err});
|
||||
logger.info("sendLightningReport: {!}", .{err});
|
||||
self.processLndReportError(err) catch |err2| logger.err("processLndReportError: {!}", .{err2});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -363,7 +412,7 @@ fn commThreadLoop(self: *Daemon) void {
|
|||
}
|
||||
switch (self.state) {
|
||||
.stopped, .poweroff => break :loop,
|
||||
.running, .standby => {
|
||||
.running, .standby, .wallet_reset => {
|
||||
logger.err("commThreadLoop: {any}", .{err});
|
||||
if (err == error.EndOfStream) {
|
||||
// pointless to continue running if comms I/O is broken.
|
||||
|
@ -407,6 +456,27 @@ fn commThreadLoop(self: *Daemon) void {
|
|||
// TODO: send err back to ngui
|
||||
};
|
||||
},
|
||||
.lightning_genseed => {
|
||||
self.generateWalletSeed() catch |err| {
|
||||
logger.err("generateWalletSeed: {!}", .{err});
|
||||
// TODO: send err back to ngui
|
||||
};
|
||||
},
|
||||
.lightning_init_wallet => |req| {
|
||||
self.initWallet(req) catch |err| {
|
||||
logger.err("initWallet: {!}", .{err});
|
||||
// TODO: send err back to ngui
|
||||
};
|
||||
},
|
||||
.lightning_get_ctrlconn => {
|
||||
self.sendLightningPairingConn() catch |err| {
|
||||
logger.err("sendLightningPairingConn: {!}", .{err});
|
||||
// TODO: send err back to ngui
|
||||
};
|
||||
},
|
||||
.lightning_reset => {
|
||||
self.resetLndNode() catch |err| logger.err("resetLndNode: {!}", .{err});
|
||||
},
|
||||
else => |v| logger.warn("unhandled msg tag {s}", .{@tagName(v)}),
|
||||
}
|
||||
|
||||
|
@ -420,9 +490,9 @@ fn commThreadLoop(self: *Daemon) void {
|
|||
|
||||
/// sends poweroff progress to uiwriter in comm.Message.PoweroffProgress format.
|
||||
fn sendPoweroffReport(self: *Daemon) !void {
|
||||
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
|
||||
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.list.len);
|
||||
defer self.allocator.free(svstat);
|
||||
for (self.services, svstat) |*sv, *stat| {
|
||||
for (self.services.list, svstat) |*sv, *stat| {
|
||||
stat.* = .{
|
||||
.name = sv.name,
|
||||
.stopped = sv.status() == .stopped,
|
||||
|
@ -490,7 +560,7 @@ fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
|||
/// initiates wifi connection procedure in a separate thread
|
||||
fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !void {
|
||||
if (ssid.len == 0) {
|
||||
return error.ConnectWifiEmptySSID;
|
||||
return Error.ConnectWifiEmptySSID;
|
||||
}
|
||||
const ssid_copy = try self.allocator.dupe(u8, ssid);
|
||||
const pwd_copy = try self.allocator.dupe(u8, password);
|
||||
|
@ -566,6 +636,7 @@ fn readWPACtrlMsg(self: *Daemon) !void {
|
|||
}
|
||||
}
|
||||
|
||||
/// callers must hold self.mu due to self.state read access via fetchOnchainStats.
|
||||
fn sendOnchainReport(self: *Daemon) !void {
|
||||
const stats = self.fetchOnchainStats() catch |err| {
|
||||
switch (err) {
|
||||
|
@ -631,6 +702,7 @@ const OnchainStats = struct {
|
|||
balance: ?lndhttp.Client.Result(.walletbalance),
|
||||
};
|
||||
|
||||
/// call site must hold self.mu due to self.state read access.
|
||||
/// callers own returned value.
|
||||
fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
||||
var client = bitcoindrpc.Client{
|
||||
|
@ -642,10 +714,14 @@ fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
|||
const mempool = try client.call(.getmempoolinfo, {});
|
||||
|
||||
const balance: ?lndhttp.Client.Result(.walletbalance) = blk: { // lndhttp.WalletBalance
|
||||
if (self.state == .wallet_reset) {
|
||||
break :blk null;
|
||||
}
|
||||
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",
|
||||
.tlscert_path = Config.LND_TLSCERT_PATH,
|
||||
.macaroon_ro_path = Config.LND_MACAROON_RO_PATH,
|
||||
.macaroon_admin_path = Config.LND_MACAROON_ADMIN_PATH,
|
||||
}) catch break :blk null;
|
||||
defer lndc.deinit();
|
||||
const res = lndc.call(.walletbalance, {}) catch break :blk null;
|
||||
|
@ -662,8 +738,9 @@ fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
|||
fn sendLightningReport(self: *Daemon) !void {
|
||||
var client = try lndhttp.Client.init(.{
|
||||
.allocator = self.allocator,
|
||||
.tlscert_path = "/home/lnd/.lnd/tls.cert",
|
||||
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
|
||||
.tlscert_path = Config.LND_TLSCERT_PATH,
|
||||
.macaroon_ro_path = Config.LND_MACAROON_RO_PATH,
|
||||
.macaroon_admin_path = Config.LND_MACAROON_ADMIN_PATH,
|
||||
});
|
||||
defer client.deinit();
|
||||
|
||||
|
@ -801,6 +878,238 @@ fn sendLightningReport(self: *Daemon) !void {
|
|||
try comm.write(self.allocator, self.uiwriter, .{ .lightning_report = lndrep });
|
||||
}
|
||||
|
||||
/// evaluates any error returned from `sendLightningReport`.
|
||||
/// callers must hold self.mu.
|
||||
fn processLndReportError(self: *Daemon, err: anyerror) !void {
|
||||
const msg_starting: comm.Message = .{ .lightning_error = .{ .code = .not_ready } };
|
||||
const msg_locked: comm.Message = .{ .lightning_error = .{ .code = .locked } };
|
||||
const msg_uninitialized: comm.Message = .{ .lightning_error = .{ .code = .uninitialized } };
|
||||
|
||||
switch (err) {
|
||||
error.ConnectionRefused,
|
||||
error.FileNotFound, // tls cert file missing, not re-generated by lnd yet
|
||||
=> return comm.write(self.allocator, self.uiwriter, msg_starting),
|
||||
// old tls cert, refused by our http client
|
||||
std.http.Client.ConnectUnproxiedError.TlsInitializationFailed => {
|
||||
try self.resetLndTlsUnguarded();
|
||||
return error.LndReportRetryLater;
|
||||
},
|
||||
else => {}, // continue
|
||||
}
|
||||
|
||||
// checking wallet status requires no macaroon auth
|
||||
var client = try lndhttp.Client.init(.{ .allocator = self.allocator, .tlscert_path = Config.LND_TLSCERT_PATH });
|
||||
defer client.deinit();
|
||||
const status = client.call(.walletstatus, {}) catch |err2| {
|
||||
switch (err2) {
|
||||
error.TlsInitializationFailed => {
|
||||
try self.resetLndTlsUnguarded();
|
||||
return error.LndReportRetryLater;
|
||||
},
|
||||
else => return err2,
|
||||
}
|
||||
};
|
||||
defer status.deinit();
|
||||
logger.info("processLndReportError: lnd wallet state: {s}", .{@tagName(status.value.state)});
|
||||
return switch (status.value.state) {
|
||||
.NON_EXISTING => {
|
||||
try comm.write(self.allocator, self.uiwriter, msg_uninitialized);
|
||||
self.lnd_timer.reset();
|
||||
self.want_lnd_report = false;
|
||||
},
|
||||
.LOCKED => {
|
||||
try comm.write(self.allocator, self.uiwriter, msg_locked);
|
||||
self.lnd_timer.reset();
|
||||
self.want_lnd_report = false;
|
||||
},
|
||||
.UNLOCKED, .RPC_ACTIVE, .WAITING_TO_START => comm.write(self.allocator, self.uiwriter, msg_starting),
|
||||
// active server indicates the lnd is ready to accept calls. so, the error
|
||||
// must have been due to factors other than unoperational lnd state.
|
||||
.SERVER_ACTIVE => err,
|
||||
};
|
||||
}
|
||||
|
||||
fn sendLightningPairingConn(self: *Daemon) !void {
|
||||
const tor_rpc = try self.conf.lndConnectWaitMacaroonFile(self.allocator, .tor_rpc);
|
||||
defer self.allocator.free(tor_rpc);
|
||||
const tor_http = try self.conf.lndConnectWaitMacaroonFile(self.allocator, .tor_http);
|
||||
defer self.allocator.free(tor_http);
|
||||
var conn: comm.Message.LightningCtrlConn = &.{
|
||||
.{ .url = tor_rpc, .typ = .lnd_rpc, .perm = .admin },
|
||||
.{ .url = tor_http, .typ = .lnd_http, .perm = .admin },
|
||||
};
|
||||
try comm.write(self.allocator, self.uiwriter, .{ .lightning_ctrlconn = conn });
|
||||
}
|
||||
|
||||
/// a non-committal seed generator. can be called any number of times.
|
||||
fn generateWalletSeed(self: *Daemon) !void {
|
||||
// genseed needs no auth
|
||||
var client = try lndhttp.Client.init(.{ .allocator = self.allocator, .tlscert_path = Config.LND_TLSCERT_PATH });
|
||||
defer client.deinit();
|
||||
const res = try client.call(.genseed, {});
|
||||
defer res.deinit();
|
||||
const msg = comm.Message{ .lightning_genseed_result = res.value.cipher_seed_mnemonic };
|
||||
return comm.write(self.allocator, self.uiwriter, msg);
|
||||
}
|
||||
|
||||
/// commit req.mnemonic as the new lightning wallet.
|
||||
/// this also creates a new unlock password, placing it at `Config.LND_WALLETUNLOCK_PATH`,
|
||||
/// and finally generates a new lnd config file to persist the changes.
|
||||
fn initWallet(self: *Daemon, req: comm.Message.LightningInitWallet) !void {
|
||||
self.mu.lock();
|
||||
switch (self.state) {
|
||||
.stopped, .poweroff, .wallet_reset => {
|
||||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.poweroff => return Error.PoweroffActive,
|
||||
.wallet_reset => return Error.WalletResetActive,
|
||||
else => return Error.InvalidState,
|
||||
}
|
||||
},
|
||||
// proceed only when in one of the following states
|
||||
.standby => screen.backlight(.on) catch |err| logger.err("initWallet: backlight on: {!}", .{err}),
|
||||
.running => {},
|
||||
}
|
||||
defer {
|
||||
self.mu.lock();
|
||||
self.state = .running;
|
||||
self.mu.unlock();
|
||||
}
|
||||
self.state = .wallet_reset;
|
||||
self.mu.unlock();
|
||||
|
||||
// generate a new wallet unlock password; used together with seed committal below.
|
||||
var buf: [128]u8 = undefined;
|
||||
const unlock_pwd = self.conf.makeWalletUnlockFile(&buf, 8) catch |err| {
|
||||
logger.err("makeWalletUnlockFile: {!}", .{err});
|
||||
return Error.MakeWalletUnlockFileFail;
|
||||
};
|
||||
|
||||
// commit the seed: initwallet needs no auth
|
||||
logger.info("initwallet: committing new seed and an unlock password", .{});
|
||||
var client = try lndhttp.Client.init(.{ .allocator = self.allocator, .tlscert_path = Config.LND_TLSCERT_PATH });
|
||||
defer client.deinit();
|
||||
const res = client.call(.initwallet, .{ .unlock_password = unlock_pwd, .mnemonic = req.mnemonic }) catch |err| {
|
||||
logger.err("lnd client initwallet: {!}", .{err});
|
||||
return Error.InitLndWallet;
|
||||
};
|
||||
res.deinit(); // unused
|
||||
|
||||
// generate a valid lnd config before unlocking the first time but without auto-unlock.
|
||||
// the latter works only after first unlock - see below.
|
||||
//
|
||||
// important details about a "valid" config is lnd needs correct bitcoind rpc auth,
|
||||
// which historically has been missing at the initial OS image build.
|
||||
logger.info("initwallet: generating lnd config file without auto-unlock", .{});
|
||||
try self.conf.genLndConfig(.{ .autounlock = false });
|
||||
|
||||
// restart the lnd service to pick up the newly generated config above.
|
||||
logger.info("initwallet: restarting lnd", .{});
|
||||
try self.services.stopWait(SysService.LND);
|
||||
try self.services.start(SysService.LND);
|
||||
var timer = try types.Timer.start();
|
||||
while (timer.read() < 10 * time.ns_per_s) {
|
||||
const status = client.call(.walletstatus, {}) catch |err| {
|
||||
logger.info("initwallet: waiting lnd restart: {!}", .{err});
|
||||
std.time.sleep(1 * time.ns_per_s);
|
||||
continue;
|
||||
};
|
||||
defer status.deinit();
|
||||
switch (status.value.state) {
|
||||
.LOCKED => break,
|
||||
else => |t| {
|
||||
logger.info("initwallet: waiting lnd restart: {s}", .{@tagName(t)});
|
||||
std.time.sleep(1 * time.ns_per_s);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// unlock the wallet for the first time: required after initwallet.
|
||||
// it generates macaroon files and completes a wallet initialization.
|
||||
logger.info("initwallet: unlocking new wallet for the first time", .{});
|
||||
const res2 = client.call(.unlockwallet, .{ .unlock_password = unlock_pwd }) catch |err| {
|
||||
logger.err("lnd client unlockwallet: {!}", .{err});
|
||||
return Error.UnlockLndWallet;
|
||||
};
|
||||
res2.deinit(); // unused
|
||||
|
||||
// same as above genLndConfig but with auto-unlock enabled.
|
||||
// no need to restart lnd: it'll pick up the new config on next boot.
|
||||
logger.info("initwallet: re-generating lnd config with auto-unlock", .{});
|
||||
try self.conf.genLndConfig(.{ .autounlock = true });
|
||||
}
|
||||
|
||||
/// factory-resets lnd node; wipes out the wallet.
|
||||
fn resetLndNode(self: *Daemon) !void {
|
||||
self.mu.lock();
|
||||
switch (self.state) {
|
||||
.stopped, .poweroff, .wallet_reset => {
|
||||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.poweroff => return Error.PoweroffActive,
|
||||
.wallet_reset => return Error.WalletResetActive,
|
||||
else => return Error.InvalidState,
|
||||
}
|
||||
},
|
||||
// proceed only when in one of the following states
|
||||
.running, .standby => {},
|
||||
}
|
||||
const prevstate = self.state;
|
||||
defer {
|
||||
self.mu.lock();
|
||||
self.state = prevstate;
|
||||
self.mu.unlock();
|
||||
}
|
||||
self.state = .wallet_reset;
|
||||
self.mu.unlock();
|
||||
|
||||
// 1. stop lnd service
|
||||
try self.services.stopWait(SysService.LND);
|
||||
|
||||
// 2. delete all data directories
|
||||
try std.fs.cwd().deleteTree(Config.LND_DATA_DIR);
|
||||
try std.fs.cwd().deleteTree(Config.LND_LOG_DIR);
|
||||
try std.fs.cwd().deleteFile(Config.LND_WALLETUNLOCK_PATH);
|
||||
if (std.fs.path.dirname(Config.LND_TLSCERT_PATH)) |dir| {
|
||||
try std.fs.cwd().deleteTree(dir);
|
||||
}
|
||||
// TODO: reset tor hidden service pubkey?
|
||||
|
||||
// 3. generate a new blank config so lnd can start up again and respond
|
||||
// to status requests.
|
||||
try self.conf.genLndConfig(.{ .autounlock = false });
|
||||
|
||||
// 4. start lnd service
|
||||
try self.services.start(SysService.LND);
|
||||
}
|
||||
|
||||
/// like resetLndNode but resets only tls certs, nothing else.
|
||||
/// callers must acquire self.mu.
|
||||
fn resetLndTlsUnguarded(self: *Daemon) !void {
|
||||
if (self.lnd_tls_reset_count > 0) {
|
||||
return error.LndTlsResetCount;
|
||||
}
|
||||
switch (self.state) {
|
||||
.stopped, .poweroff, .wallet_reset => {
|
||||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.poweroff => return Error.PoweroffActive,
|
||||
.wallet_reset => return Error.WalletResetActive,
|
||||
else => return Error.InvalidState,
|
||||
}
|
||||
},
|
||||
// proceed only when in one of the following states
|
||||
.running, .standby => {},
|
||||
}
|
||||
logger.info("resetting lnd tls certs", .{});
|
||||
try std.fs.cwd().deleteFile(Config.LND_TLSKEY_PATH);
|
||||
try std.fs.cwd().deleteFile(Config.LND_TLSCERT_PATH);
|
||||
try self.services.stopWait(SysService.LND);
|
||||
try self.services.start(SysService.LND);
|
||||
self.lnd_tls_reset_count += 1;
|
||||
}
|
||||
|
||||
fn switchSysupdates(self: *Daemon, chan: comm.Message.SysupdatesChan) !void {
|
||||
const th = try std.Thread.spawn(.{}, switchSysupdatesThread, .{ self, chan });
|
||||
th.detach();
|
||||
|
@ -856,8 +1165,8 @@ test "start-stop" {
|
|||
try t.expect(!daemon.wpa_ctrl.attached);
|
||||
try t.expect(daemon.wpa_ctrl.opened);
|
||||
|
||||
try t.expect(daemon.services.len > 0);
|
||||
for (daemon.services) |*sv| {
|
||||
try t.expect(daemon.services.list.len > 0);
|
||||
for (daemon.services.list) |*sv| {
|
||||
try t.expect(!sv.stop_proc.spawned);
|
||||
try t.expectEqual(SysService.Status.initial, sv.status());
|
||||
}
|
||||
|
@ -902,7 +1211,7 @@ test "start-poweroff" {
|
|||
daemon.wait();
|
||||
try t.expect(daemon.state == .stopped);
|
||||
try t.expect(daemon.poweroff_thread == null);
|
||||
for (daemon.services) |*sv| {
|
||||
for (daemon.services.list) |*sv| {
|
||||
try t.expect(sv.stop_proc.spawned);
|
||||
try t.expect(sv.stop_proc.waited);
|
||||
try t.expectEqual(SysService.Status.stopped, sv.status());
|
||||
|
|
|
@ -3,6 +3,18 @@
|
|||
const std = @import("std");
|
||||
const types = @import("../types.zig");
|
||||
|
||||
// known service names
|
||||
pub const LND = "lnd";
|
||||
pub const BITCOIND = "bitcoind";
|
||||
|
||||
const Error = error{
|
||||
SysServiceStopInProgress,
|
||||
SysServiceBadStartCode,
|
||||
SysServiceBadStartTerm,
|
||||
SysServiceBadStopCode,
|
||||
SysServiceBadStopTerm,
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
stop_wait_sec: ?u32 = null,
|
||||
|
@ -17,13 +29,15 @@ stop_err: ?anyerror = null,
|
|||
/// the .initial value is a temporary solution until service watcher and start
|
||||
/// are implemnted: at the moment, SysService can only stop services, nothing else.
|
||||
pub const Status = enum(u8) {
|
||||
initial, // TODO: add .running
|
||||
initial, // TODO: get rid of "initial" and infer the actual state
|
||||
started,
|
||||
stopping,
|
||||
stopped,
|
||||
};
|
||||
|
||||
const State = union(Status) {
|
||||
initial: void,
|
||||
started: std.ChildProcess.Term,
|
||||
stopping: void,
|
||||
stopped: std.ChildProcess.Term,
|
||||
};
|
||||
|
@ -64,6 +78,26 @@ pub fn lastStopError(self: *SysService) ?anyerror {
|
|||
return self.stop_err;
|
||||
}
|
||||
|
||||
/// launches a service start procedure and returns as soon as the startup script
|
||||
/// terminates: whether the service actually started successefully is not necessarily
|
||||
/// indicated by the function return.
|
||||
pub fn start(self: *SysService) !void {
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
switch (self.stat) {
|
||||
.stopping => return Error.SysServiceStopInProgress,
|
||||
.initial, .started, .stopped => {}, // proceed
|
||||
}
|
||||
|
||||
var proc = types.ChildProcess.init(&.{ "sv", "start", self.name }, self.allocator);
|
||||
const term = try proc.spawnAndWait();
|
||||
self.stat = .{ .started = term };
|
||||
switch (term) {
|
||||
.Exited => |code| if (code != 0) return Error.SysServiceBadStartCode,
|
||||
else => return Error.SysServiceBadStartTerm,
|
||||
}
|
||||
}
|
||||
|
||||
/// launches a service stop procedure and returns immediately.
|
||||
/// callers must invoke stopWait to release all resources used by the stop.
|
||||
pub fn stop(self: *SysService) !void {
|
||||
|
@ -71,7 +105,7 @@ pub fn stop(self: *SysService) !void {
|
|||
defer self.mu.unlock();
|
||||
|
||||
self.stop_err = null;
|
||||
self.spawnStop() catch |err| {
|
||||
self.spawnStopUnguarded() catch |err| {
|
||||
self.stop_err = err;
|
||||
return err;
|
||||
};
|
||||
|
@ -84,7 +118,7 @@ pub fn stopWait(self: *SysService) !void {
|
|||
defer self.mu.unlock();
|
||||
|
||||
self.stop_err = null;
|
||||
self.spawnStop() catch |err| {
|
||||
self.spawnStopUnguarded() catch |err| {
|
||||
self.stop_err = err;
|
||||
return err;
|
||||
};
|
||||
|
@ -96,10 +130,10 @@ pub fn stopWait(self: *SysService) !void {
|
|||
self.stat = .{ .stopped = term };
|
||||
switch (term) {
|
||||
.Exited => |code| if (code != 0) {
|
||||
self.stop_err = error.SysServiceBadStopCode;
|
||||
self.stop_err = Error.SysServiceBadStopCode;
|
||||
},
|
||||
else => {
|
||||
self.stop_err = error.SysServiceBadStopTerm;
|
||||
self.stop_err = Error.SysServiceBadStopTerm;
|
||||
},
|
||||
}
|
||||
if (self.stop_err) |err| {
|
||||
|
@ -109,11 +143,11 @@ pub fn stopWait(self: *SysService) !void {
|
|||
|
||||
/// actual internal body of SysService.stop: stopWait also uses this.
|
||||
/// callers must hold self.mu.
|
||||
fn spawnStop(self: *SysService) !void {
|
||||
fn spawnStopUnguarded(self: *SysService) !void {
|
||||
switch (self.stat) {
|
||||
.stopping => return, // already in progress
|
||||
// intentionally let .stopped state pass through: can't see any downsides.
|
||||
.initial, .stopped => {},
|
||||
.initial, .started, .stopped => {},
|
||||
}
|
||||
|
||||
// use arena to simplify stop proc args construction.
|
||||
|
|
33
src/ngui.zig
33
src/ngui.zig
|
@ -40,7 +40,7 @@ var last_report: struct {
|
|||
mu: std.Thread.Mutex = .{},
|
||||
network: ?comm.ParsedMessage = null, // NetworkReport
|
||||
onchain: ?comm.ParsedMessage = null, // OnchainReport
|
||||
lightning: ?comm.ParsedMessage = null, // LightningReport
|
||||
lightning: ?comm.ParsedMessage = null, // LightningReport or LightningError
|
||||
|
||||
fn deinit(self: *@This()) void {
|
||||
self.mu.lock();
|
||||
|
@ -76,7 +76,7 @@ var last_report: struct {
|
|||
}
|
||||
self.onchain = new;
|
||||
},
|
||||
.lightning_report => {
|
||||
.lightning_report, .lightning_error => {
|
||||
if (self.lightning) |old| {
|
||||
old.deinit();
|
||||
}
|
||||
|
@ -233,7 +233,19 @@ fn commThreadLoopCycle() !void {
|
|||
.network_report,
|
||||
.onchain_report,
|
||||
.lightning_report,
|
||||
.lightning_error,
|
||||
=> last_report.replace(msg),
|
||||
.lightning_genseed_result,
|
||||
.lightning_ctrlconn,
|
||||
// TODO: merge standby vs active switch branches
|
||||
=> {
|
||||
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
|
||||
msg.deinit();
|
||||
},
|
||||
.settings => |sett| {
|
||||
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
|
||||
msg.deinit();
|
||||
},
|
||||
else => {
|
||||
logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)});
|
||||
msg.deinit();
|
||||
|
@ -256,10 +268,16 @@ fn commThreadLoopCycle() !void {
|
|||
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
|
||||
last_report.replace(msg);
|
||||
},
|
||||
.lightning_report => |rep| {
|
||||
ui.lightning.updateTabPanel(rep) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
|
||||
.lightning_report, .lightning_error => {
|
||||
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
|
||||
last_report.replace(msg);
|
||||
},
|
||||
.lightning_genseed_result,
|
||||
.lightning_ctrlconn,
|
||||
=> {
|
||||
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
|
||||
msg.deinit();
|
||||
},
|
||||
.settings => |sett| {
|
||||
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
|
||||
msg.deinit();
|
||||
|
@ -311,6 +329,11 @@ fn uiThreadLoop() void {
|
|||
logger.err("bitcoin.updateTabPanel: {any}", .{err});
|
||||
};
|
||||
}
|
||||
if (last_report.lightning) |msg| {
|
||||
ui.lightning.updateTabPanel(msg.value) catch |err| {
|
||||
logger.err("lightning.updateTabPanel: {any}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
},
|
||||
|
@ -383,7 +406,7 @@ pub fn main() anyerror!void {
|
|||
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
|
||||
|
||||
// initalizes display, input driver and finally creates the user interface.
|
||||
ui.init() catch |err| {
|
||||
ui.init(gpa) catch |err| {
|
||||
logger.err("ui.init: {any}", .{err});
|
||||
return err;
|
||||
};
|
||||
|
|
|
@ -118,6 +118,25 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
|
|||
} };
|
||||
comm.write(gpa, w, .{ .poweroff_progress = s3 }) catch |err| logger.err("comm.write: {any}", .{err});
|
||||
},
|
||||
.lightning_genseed => {
|
||||
time.sleep(2 * time.ns_per_s);
|
||||
comm.write(gpa, w, .{ .lightning_genseed_result = &.{
|
||||
"ability", "dance", "scatter", "raw", "fly", "dentist", "bar", "nominee",
|
||||
"exhaust", "wine", "snap", "super", "cost", "case", "coconut", "ticket",
|
||||
"spread", "funny", "grain", "chimney", "aspect", "business", "quiz", "ginger",
|
||||
} }) catch |err| logger.err("{!}", .{err});
|
||||
},
|
||||
.lightning_init_wallet => |v| {
|
||||
logger.info("mnemonic: {s}", .{v.mnemonic});
|
||||
time.sleep(3 * time.ns_per_s);
|
||||
},
|
||||
.lightning_get_ctrlconn => {
|
||||
var conn: comm.Message.LightningCtrlConn = &.{
|
||||
.{ .url = "lndconnect://adfkjhadwaepoijsadflkjtrpoijawokjafulkjsadfkjhgjfdskjszd.onion:10009?macaroon=Adasjsadkfljhfjhasdpiuhfiuhawfffoihgpoiadsfjharpoiuhfdsgpoihafdsgpoiheafoiuhasdfhisdufhiuhfewiuhfiuhrfl6prrx", .typ = .lnd_rpc, .perm = .admin },
|
||||
.{ .url = "lndconnect://adfkjhadwaepoijsadflkjtrpoijawokjafulkjsadfkjhgjfdskjszd.onion:10010?macaroon=Adasjsadkfljhfjhasdpiuhfiuhawfffoihgpoiadsfjharpoiuhfdsgpoihafdsgpoiheafoiuhasdfhisdufhiuhfewiuhfiuhrfl6prrx", .typ = .lnd_http, .perm = .admin },
|
||||
};
|
||||
comm.write(gpa, w, .{ .lightning_ctrlconn = conn }) catch |err| logger.err("{!}", .{err});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
@ -131,6 +150,8 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
|
|||
var block_count: u32 = 801365;
|
||||
var settings_sent = false;
|
||||
|
||||
var lnd_uninited_sent = false;
|
||||
|
||||
while (true) {
|
||||
time.sleep(time.ns_per_s);
|
||||
if (sectimer.read() < 3 * time.ns_per_s) {
|
||||
|
@ -185,6 +206,11 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
|
|||
};
|
||||
comm.write(gpa, w, .{ .onchain_report = btcrep }) catch |err| logger.err("comm.write: {any}", .{err});
|
||||
|
||||
if (!lnd_uninited_sent and block_count % 2 == 0) {
|
||||
comm.write(gpa, w, .{ .lightning_error = .{ .code = .uninitialized } }) catch |err| logger.err("{any}", .{err});
|
||||
lnd_uninited_sent = true;
|
||||
}
|
||||
|
||||
if (block_count % 2 == 0) {
|
||||
const lndrep: comm.Message.LightningReport = .{
|
||||
.version = "0.16.4-beta commit=v0.16.4-beta",
|
||||
|
|
|
@ -10,11 +10,22 @@ pub fn main() !void {
|
|||
|
||||
var client = try lndhttp.Client.init(.{
|
||||
.allocator = gpa,
|
||||
.port = 10010,
|
||||
.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(.walletstatus, {});
|
||||
defer res.deinit();
|
||||
std.debug.print("{}\n", .{res.value.state});
|
||||
|
||||
if (res.value.state == .LOCKED) {
|
||||
const res2 = try client.call(.unlockwallet, .{ .unlock_password = "45b08eb7bfcf1a7f" });
|
||||
defer res2.deinit();
|
||||
}
|
||||
}
|
||||
{
|
||||
const res = try client.call(.getinfo, {});
|
||||
defer res.deinit();
|
||||
|
@ -31,13 +42,13 @@ pub fn main() !void {
|
|||
// 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});
|
||||
//}
|
||||
{
|
||||
const res = try client.call(.genseed, {});
|
||||
defer res.deinit();
|
||||
std.debug.print("{s}\n", .{res.value.cipher_seed_mnemonic});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ pub const IoPipe = struct {
|
|||
}
|
||||
};
|
||||
|
||||
// TODO: turns this into a UniqStringList backed by StringArrayHashMap; also see std.BufSet
|
||||
pub const StringList = struct {
|
||||
l: std.ArrayList([]const u8),
|
||||
allocator: std.mem.Allocator,
|
||||
|
@ -62,7 +61,17 @@ pub const StringList = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
/// duplicates unowned items into the returned list.
|
||||
pub fn fromUnowned(allocator: std.mem.Allocator, unowned: []const []const u8) !Self {
|
||||
var list = Self.init(allocator);
|
||||
errdefer list.deinit();
|
||||
for (unowned) |item| {
|
||||
try list.append(item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
for (self.l.items) |a| {
|
||||
self.allocator.free(a);
|
||||
}
|
||||
|
|
|
@ -605,7 +605,7 @@
|
|||
#define LV_USE_GIF 0
|
||||
|
||||
/*QR code library*/
|
||||
#define LV_USE_QRCODE 0
|
||||
#define LV_USE_QRCODE 1
|
||||
|
||||
/*FreeType library*/
|
||||
#define LV_USE_FREETYPE 0
|
||||
|
|
|
@ -92,6 +92,16 @@ extern lv_style_t *nm_style_title()
|
|||
return &style_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* a hack to prevent tabview from switching to the next tab
|
||||
* on a scroll event, for example coming from a top layer window.
|
||||
* call this func after deleting all children of an object.
|
||||
*/
|
||||
extern void preserve_main_active_tab()
|
||||
{
|
||||
lv_tabview_set_act(tabview, lv_tabview_get_tab_act(tabview), LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
static void textarea_event_cb(lv_event_t *e)
|
||||
{
|
||||
lv_obj_t *textarea = lv_event_get_target(e);
|
||||
|
|
|
@ -5,15 +5,52 @@ const std = @import("std");
|
|||
|
||||
const comm = @import("../comm.zig");
|
||||
const lvgl = @import("lvgl.zig");
|
||||
const symbol = @import("symbol.zig");
|
||||
const types = @import("../types.zig");
|
||||
const xfmt = @import("../xfmt.zig");
|
||||
const widget = @import("widget.zig");
|
||||
|
||||
const logger = std.log.scoped(.ui_lnd);
|
||||
|
||||
const appBitBanana = "BitBanana";
|
||||
const appZeus = "Zeus";
|
||||
const app_description = std.ComptimeStringMap([:0]const u8, .{
|
||||
.{
|
||||
appBitBanana,
|
||||
\\lightning node management for android.
|
||||
\\website: https://bitbanana.app
|
||||
\\
|
||||
\\follow the website instructions for
|
||||
\\installing the app. once installed,
|
||||
\\scan the QR code using the app and
|
||||
\\complete the setup process.
|
||||
},
|
||||
.{
|
||||
appZeus,
|
||||
\\available for android and iOS.
|
||||
\\website: https://zeusln.app
|
||||
\\
|
||||
\\follow the website instructions for
|
||||
\\installing the app. once installed,
|
||||
\\scan the QR code using the app and
|
||||
\\complete the setup process.
|
||||
},
|
||||
});
|
||||
|
||||
/// label color mark start to make "label:" part of a "label: value"
|
||||
/// in a different color.
|
||||
const cmark = "#bbbbbb ";
|
||||
|
||||
/// a hack to prevent tabview from switching to the next tab
|
||||
/// on a scroll event when deleting all children of tab.seed_setup.topwin.
|
||||
/// defined in ui/c/ui.c
|
||||
extern fn preserve_main_active_tab() void;
|
||||
|
||||
var tab: struct {
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
info: struct {
|
||||
card: lvgl.Card, // parent
|
||||
alias: lvgl.Label,
|
||||
blockhash: lvgl.Label,
|
||||
currblock: lvgl.Label,
|
||||
|
@ -22,6 +59,7 @@ var tab: struct {
|
|||
version: lvgl.Label,
|
||||
},
|
||||
balance: struct {
|
||||
card: lvgl.Card, // parent
|
||||
avail: lvgl.Bar, // local vs remote
|
||||
local: lvgl.Label,
|
||||
remote: lvgl.Label,
|
||||
|
@ -29,18 +67,119 @@ var tab: struct {
|
|||
pending: lvgl.Label,
|
||||
fees: lvgl.Label, // day, week, month
|
||||
},
|
||||
channels_cont: lvgl.FlexLayout,
|
||||
channels: struct {
|
||||
card: lvgl.Card,
|
||||
cont: lvgl.FlexLayout,
|
||||
},
|
||||
pairing: lvgl.Card,
|
||||
reset: lvgl.Card,
|
||||
|
||||
// elements visibile during lnd startup.
|
||||
startup: lvgl.FlexLayout,
|
||||
|
||||
// TODO: support wallet manual unlock (LightningError.code.Locked)
|
||||
|
||||
// elements when lnd wallet is uninitialized.
|
||||
// the actual seed init is in `seed_setup` field.
|
||||
nowallet: lvgl.FlexLayout,
|
||||
|
||||
seed_setup: ?struct {
|
||||
topwin: lvgl.Window,
|
||||
arena: *std.heap.ArenaAllocator, // all non-UI elements are alloc'ed here
|
||||
mnemonic: ?types.StringList = null, // 24 words genseed result
|
||||
pairing: ?struct {
|
||||
// app_description key to connection URL.
|
||||
// keys are static, values are heap-alloc'ed in `setupPairing`.
|
||||
urlmap: std.StringArrayHashMap([]const u8),
|
||||
appsel: lvgl.Dropdown,
|
||||
appdesc: lvgl.Label,
|
||||
qr: lvgl.QrCode, // QR-encoded connection URL
|
||||
qrerr: lvgl.Label, // an error message when QR rendering fails
|
||||
} = null,
|
||||
} = null,
|
||||
|
||||
fn initSetup(self: *@This(), topwin: lvgl.Window) !void {
|
||||
var arena = try self.allocator.create(std.heap.ArenaAllocator);
|
||||
arena.* = std.heap.ArenaAllocator.init(tab.allocator);
|
||||
self.seed_setup = .{ .arena = arena, .topwin = topwin };
|
||||
}
|
||||
|
||||
fn destroySetup(self: *@This()) void {
|
||||
if (self.seed_setup == null) {
|
||||
return;
|
||||
}
|
||||
self.seed_setup.?.topwin.destroy();
|
||||
self.seed_setup.?.arena.deinit();
|
||||
self.allocator.destroy(self.seed_setup.?.arena);
|
||||
self.seed_setup = null;
|
||||
preserve_main_active_tab();
|
||||
}
|
||||
|
||||
fn setMode(self: *@This(), m: enum { setup, startup, operational }) void {
|
||||
switch (m) {
|
||||
.setup => {
|
||||
self.nowallet.show();
|
||||
self.startup.hide();
|
||||
self.info.card.hide();
|
||||
self.balance.card.hide();
|
||||
self.channels.card.hide();
|
||||
self.pairing.hide();
|
||||
self.reset.hide();
|
||||
},
|
||||
.startup => {
|
||||
self.startup.show();
|
||||
self.nowallet.hide();
|
||||
self.info.card.hide();
|
||||
self.balance.card.hide();
|
||||
self.channels.card.hide();
|
||||
self.pairing.hide();
|
||||
self.reset.hide();
|
||||
},
|
||||
.operational => {
|
||||
self.info.card.show();
|
||||
self.balance.card.show();
|
||||
self.channels.card.show();
|
||||
self.pairing.show();
|
||||
self.reset.show();
|
||||
self.nowallet.hide();
|
||||
self.startup.hide();
|
||||
},
|
||||
}
|
||||
}
|
||||
} = undefined;
|
||||
|
||||
/// creates the tab content with all elements.
|
||||
/// must be called only once at UI init.
|
||||
pub fn initTabPanel(cont: lvgl.Container) !void {
|
||||
pub fn initTabPanel(allocator: std.mem.Allocator, cont: lvgl.Container) !void {
|
||||
tab.allocator = allocator;
|
||||
const parent = cont.flex(.column, .{});
|
||||
const recolor: lvgl.Label.Opt = .{ .recolor = true };
|
||||
|
||||
// startup
|
||||
{
|
||||
tab.startup = try lvgl.FlexLayout.new(parent, .row, .{ .all = .center });
|
||||
tab.startup.resizeToMax();
|
||||
_ = try lvgl.Spinner.new(tab.startup);
|
||||
_ = try lvgl.Label.new(tab.startup, "STARTING UP ...", .{});
|
||||
}
|
||||
|
||||
// uninitialized wallet state
|
||||
{
|
||||
tab.nowallet = try lvgl.FlexLayout.new(parent, .column, .{ .all = .center });
|
||||
tab.nowallet.resizeToMax();
|
||||
tab.nowallet.setPad(10, .row, .{});
|
||||
_ = try lvgl.Label.new(tab.nowallet, "lightning wallet is uninitialized.\ntap the button to start the setup process.", .{});
|
||||
const btn = try lvgl.TextButton.new(tab.nowallet, "SETUP NEW WALLET");
|
||||
btn.setWidth(lvgl.sizePercent(50));
|
||||
_ = btn.on(.click, nm_lnd_setup_click, null);
|
||||
}
|
||||
|
||||
// regular operational mode
|
||||
|
||||
// info section
|
||||
{
|
||||
const card = try lvgl.Card.new(parent, "INFO", .{});
|
||||
const row = try lvgl.FlexLayout.new(card, .row, .{});
|
||||
tab.info.card = try lvgl.Card.new(parent, "INFO", .{});
|
||||
const row = try lvgl.FlexLayout.new(tab.info.card, .row, .{});
|
||||
row.setHeightToContent();
|
||||
row.setWidth(lvgl.sizePercent(100));
|
||||
row.clearFlag(.scrollable);
|
||||
|
@ -49,22 +188,22 @@ pub fn initTabPanel(cont: lvgl.Container) !void {
|
|||
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 });
|
||||
tab.info.alias = try lvgl.Label.new(left, "ALIAS\n", recolor);
|
||||
tab.info.pubkey = try lvgl.Label.new(left, "PUBKEY\n", recolor);
|
||||
tab.info.version = try lvgl.Label.new(left, "VERSION\n", recolor);
|
||||
// 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 });
|
||||
tab.info.currblock = try lvgl.Label.new(right, "HEIGHT\n", recolor);
|
||||
tab.info.blockhash = try lvgl.Label.new(right, "BLOCK HASH\n", recolor);
|
||||
tab.info.npeers = try lvgl.Label.new(right, "CONNECTED PEERS\n", recolor);
|
||||
}
|
||||
// balance section
|
||||
{
|
||||
const card = try lvgl.Card.new(parent, "BALANCE", .{});
|
||||
const row = try lvgl.FlexLayout.new(card, .row, .{});
|
||||
tab.balance.card = try lvgl.Card.new(parent, "BALANCE", .{});
|
||||
const row = try lvgl.FlexLayout.new(tab.balance.card, .row, .{});
|
||||
row.setWidth(lvgl.sizePercent(100));
|
||||
row.clearFlag(.scrollable);
|
||||
// left column
|
||||
|
@ -76,31 +215,298 @@ pub fn initTabPanel(cont: lvgl.Container) !void {
|
|||
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 });
|
||||
tab.balance.local = try lvgl.Label.new(subrow, "LOCAL\n", recolor);
|
||||
tab.balance.remote = try lvgl.Label.new(subrow, "REMOTE\n", recolor);
|
||||
// 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 });
|
||||
tab.balance.pending = try lvgl.Label.new(right, "PENDING\n", recolor);
|
||||
tab.balance.unsettled = try lvgl.Label.new(right, "UNSETTLED\n", recolor);
|
||||
// bottom
|
||||
tab.balance.fees = try lvgl.Label.new(card, "ACCUMULATED FORWARDING FEES\n", .{ .recolor = true });
|
||||
tab.balance.fees = try lvgl.Label.new(tab.balance.card, "ACCUMULATED FORWARDING FEES\n", recolor);
|
||||
}
|
||||
// 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, .{});
|
||||
tab.channels.card = try lvgl.Card.new(parent, "CHANNELS", .{});
|
||||
tab.channels.cont = try lvgl.FlexLayout.new(tab.channels.card, .column, .{});
|
||||
tab.channels.cont.setHeightToContent();
|
||||
tab.channels.cont.setWidth(lvgl.sizePercent(100));
|
||||
tab.channels.cont.clearFlag(.scrollable);
|
||||
tab.channels.cont.setPad(10, .row, .{});
|
||||
}
|
||||
// pairing section
|
||||
{
|
||||
tab.pairing = try lvgl.Card.new(parent, "PAIRING", .{});
|
||||
const row = try lvgl.FlexLayout.new(tab.pairing, .row, .{ .width = lvgl.sizePercent(100), .height = .content });
|
||||
const lb = try lvgl.Label.new(row, "tap the button on the right to start pairing with a phone.", .{});
|
||||
lb.flexGrow(1);
|
||||
const btn = try lvgl.TextButton.new(row, "PAIR");
|
||||
btn.flexGrow(1);
|
||||
_ = btn.on(.click, nm_lnd_pair_click, null);
|
||||
}
|
||||
// node reset section
|
||||
{
|
||||
tab.reset = try lvgl.Card.new(parent, symbol.Warning ++ " FACTORY RESET", .{});
|
||||
const row = try lvgl.FlexLayout.new(tab.reset, .row, .{ .width = lvgl.sizePercent(100), .height = .content });
|
||||
const lb = try lvgl.Label.new(row, "resetting the node restores its state to a factory setup.", .{});
|
||||
lb.flexGrow(1);
|
||||
const btn = try lvgl.TextButton.new(row, "RESET");
|
||||
btn.flexGrow(1);
|
||||
btn.addStyle(lvgl.nm_style_btn_red(), .{});
|
||||
_ = btn.on(.click, nm_lnd_reset_click, null);
|
||||
}
|
||||
|
||||
tab.setMode(.startup);
|
||||
}
|
||||
|
||||
/// updates the tab with new data from a `comm.Message` tagged with .lightning_xxx,
|
||||
/// the tab must be inited first with initTabPanel.
|
||||
pub fn updateTabPanel(msg: comm.Message) !void {
|
||||
return switch (msg) {
|
||||
.lightning_error => |lnerr| switch (lnerr.code) {
|
||||
.uninitialized => tab.setMode(.setup),
|
||||
// TODO: handle "wallet locked" and other errors
|
||||
else => tab.setMode(.startup),
|
||||
},
|
||||
.lightning_report => |rep| blk: {
|
||||
tab.setMode(.operational);
|
||||
break :blk updateReport(rep);
|
||||
},
|
||||
.lightning_genseed_result => |mnemonic| confirmSetupSeed(mnemonic),
|
||||
.lightning_ctrlconn => |conn| setupPairing(conn),
|
||||
else => error.UnsupportedMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export fn nm_lnd_setup_click(_: *lvgl.LvEvent) void {
|
||||
startSeedSetup() catch |err| logger.err("startSeedSetup: {any}", .{err});
|
||||
}
|
||||
|
||||
export fn nm_lnd_pair_click(_: *lvgl.LvEvent) void {
|
||||
startPairing() catch |err| logger.err("startPairing: {any}", .{err});
|
||||
}
|
||||
|
||||
export fn nm_lnd_reset_click(_: *lvgl.LvEvent) void {
|
||||
promptNodeReset() catch |err| logger.err("resetNode: {any}", .{err});
|
||||
}
|
||||
|
||||
export fn nm_lnd_setup_finish(_: *lvgl.LvEvent) void {
|
||||
tab.destroySetup();
|
||||
}
|
||||
|
||||
fn startSeedSetup() !void {
|
||||
const win = try lvgl.Window.newTop(60, " " ++ symbol.LightningBolt ++ " LIGHTNING SETUP");
|
||||
try tab.initSetup(win);
|
||||
errdefer tab.destroySetup(); // TODO: display an error instead
|
||||
const wincont = win.content().flex(.row, .{ .all = .center });
|
||||
_ = try lvgl.Spinner.new(wincont);
|
||||
_ = try lvgl.Label.new(wincont, "GENERATING SEED ...", .{});
|
||||
try comm.pipeWrite(.lightning_genseed);
|
||||
}
|
||||
|
||||
/// similar to `startSeedSetup` but used when seed is already setup,
|
||||
/// at any time later.
|
||||
/// reuses tab.seed_setup elements for the same purpose.
|
||||
fn startPairing() !void {
|
||||
const win = try lvgl.Window.newTop(60, " " ++ symbol.LightningBolt ++ " PAIRING SETUP");
|
||||
try tab.initSetup(win);
|
||||
errdefer tab.destroySetup(); // TODO: display an error instead
|
||||
const wincont = win.content().flex(.row, .{ .all = .center });
|
||||
_ = try lvgl.Spinner.new(wincont);
|
||||
_ = try lvgl.Label.new(wincont, "GATHERING CONNECTION DATA ...", .{});
|
||||
try comm.pipeWrite(.lightning_get_ctrlconn);
|
||||
}
|
||||
|
||||
fn confirmSetupSeed(mnemonic: []const []const u8) !void {
|
||||
errdefer tab.destroySetup(); // TODO: display an error instead
|
||||
if (tab.seed_setup == null) {
|
||||
return error.LightningSetupInactive;
|
||||
}
|
||||
if (mnemonic.len != 24) {
|
||||
return error.InvalidMnemonicLen;
|
||||
}
|
||||
tab.seed_setup.?.mnemonic = try types.StringList.fromUnowned(tab.seed_setup.?.arena.allocator(), mnemonic);
|
||||
|
||||
const wincont = tab.seed_setup.?.topwin.content().flex(.column, .{});
|
||||
wincont.deleteChildren();
|
||||
preserve_main_active_tab();
|
||||
|
||||
_ = try lvgl.Label.new(wincont,
|
||||
\\the seed below is the master key of this lightning node, allowing
|
||||
\\FULL CONTROL as well as recovery of funds in case of a device fatal failure.
|
||||
, .{});
|
||||
|
||||
const seedcard = try lvgl.Card.new(wincont, "SEED", .{});
|
||||
const seedcols = try lvgl.FlexLayout.new(seedcard, .row, .{ .width = lvgl.sizePercent(100), .height = .content });
|
||||
const cols: [3]lvgl.FlexLayout = .{
|
||||
try lvgl.FlexLayout.new(seedcols, .column, .{ .width = lvgl.sizePercent(30), .height = .content }),
|
||||
try lvgl.FlexLayout.new(seedcols, .column, .{ .width = lvgl.sizePercent(30), .height = .content }),
|
||||
try lvgl.FlexLayout.new(seedcols, .column, .{ .width = lvgl.sizePercent(30), .height = .content }),
|
||||
};
|
||||
var buf: [32]u8 = undefined;
|
||||
for (tab.seed_setup.?.mnemonic.?.items(), 0..) |word, i| {
|
||||
_ = try lvgl.Label.newFmt(cols[i / 8], &buf, "{d: >2}. {s}", .{ i + 1, word }, .{});
|
||||
}
|
||||
|
||||
_ = try lvgl.Label.new(wincont,
|
||||
\\it is DISPLAYED ONCE, this time only. the recommendation is to copy it
|
||||
\\over to a non-digital medium, away from easily accessible places.
|
||||
\\failure to secure the seed leads to UNRECOVERABLE LOSS of all funds.
|
||||
, .{});
|
||||
|
||||
const btnrow = try lvgl.FlexLayout.new(wincont, .row, .{
|
||||
.width = lvgl.sizePercent(100),
|
||||
.height = .content,
|
||||
.main = .space_between,
|
||||
});
|
||||
const cancel_btn = try lvgl.TextButton.new(btnrow, "CANCEL");
|
||||
cancel_btn.setWidth(lvgl.sizePercent(30));
|
||||
cancel_btn.addStyle(lvgl.nm_style_btn_red(), .{});
|
||||
_ = cancel_btn.on(.click, nm_lnd_setup_finish, null);
|
||||
|
||||
const proceed_btn = try lvgl.TextButton.new(btnrow, "PROCEED " ++ symbol.Right);
|
||||
proceed_btn.setWidth(lvgl.sizePercent(30));
|
||||
_ = proceed_btn.on(.click, nm_lnd_setup_commit_seed, null);
|
||||
}
|
||||
|
||||
export fn nm_lnd_setup_commit_seed(_: *lvgl.LvEvent) void {
|
||||
setupCommitSeed() catch |err| logger.err("setupCommitSeed: {any}", .{err});
|
||||
}
|
||||
|
||||
fn setupCommitSeed() !void {
|
||||
errdefer tab.destroySetup(); // TODO: display an error instead
|
||||
if (tab.seed_setup == null) {
|
||||
return error.LightningSetupInactive;
|
||||
}
|
||||
if (tab.seed_setup.?.mnemonic == null) {
|
||||
return error.LightningSetupNullMnemonic;
|
||||
}
|
||||
const wincont = tab.seed_setup.?.topwin.content().flex(.row, .{ .all = .center });
|
||||
wincont.deleteChildren();
|
||||
preserve_main_active_tab();
|
||||
_ = try lvgl.Spinner.new(wincont);
|
||||
_ = try lvgl.Label.new(wincont, "INITIALIZING WALLET ...", .{});
|
||||
try comm.pipeWrite(.{ .lightning_init_wallet = .{ .mnemonic = tab.seed_setup.?.mnemonic.?.items() } });
|
||||
try comm.pipeWrite(.lightning_get_ctrlconn);
|
||||
}
|
||||
|
||||
fn setupPairing(conn: comm.Message.LightningCtrlConn) !void {
|
||||
errdefer tab.destroySetup(); // TODO: display an error instead
|
||||
if (tab.seed_setup == null) {
|
||||
return error.LightningSetupInactive;
|
||||
}
|
||||
|
||||
const alloc = tab.seed_setup.?.arena.allocator();
|
||||
var urlmap = std.StringArrayHashMap([]const u8).init(alloc);
|
||||
for (conn) |ctrl| {
|
||||
if (ctrl.perm != .admin) {
|
||||
continue;
|
||||
}
|
||||
// TODO: tor vs i2p vs clearnet vs nebula
|
||||
switch (ctrl.typ) {
|
||||
.lnd_rpc => try urlmap.put(appBitBanana, try alloc.dupe(u8, ctrl.url)),
|
||||
.lnd_http => try urlmap.put(appZeus, try alloc.dupe(u8, ctrl.url)),
|
||||
}
|
||||
}
|
||||
const appsel_options = try std.mem.joinZ(alloc, "\n", urlmap.keys());
|
||||
|
||||
const wincont = tab.seed_setup.?.topwin.content().flex(.row, .{ .width = lvgl.sizePercent(100), .height = .content });
|
||||
wincont.deleteChildren();
|
||||
preserve_main_active_tab();
|
||||
|
||||
const colopt = lvgl.FlexLayout.AlignOpt{
|
||||
.width = lvgl.sizePercent(50),
|
||||
.height = .{ .fixed = lvgl.sizePercent(95) },
|
||||
};
|
||||
const leftcol = try lvgl.FlexLayout.new(wincont, .column, colopt);
|
||||
leftcol.setPad(10, .row, .{});
|
||||
const appsel = try lvgl.Dropdown.new(leftcol, appsel_options);
|
||||
appsel.setWidth(lvgl.sizePercent(100));
|
||||
_ = appsel.on(.value_changed, nm_lnd_setup_appsel_changed, null);
|
||||
const appdesc = try lvgl.Label.new(leftcol, "", .{});
|
||||
appdesc.flexGrow(1);
|
||||
const donebtn = try lvgl.TextButton.new(leftcol, "DONE");
|
||||
donebtn.setWidth(lvgl.sizePercent(100));
|
||||
_ = donebtn.on(.click, nm_lnd_setup_finish, null);
|
||||
|
||||
const rightcol = try lvgl.FlexLayout.new(wincont, .column, colopt);
|
||||
// QR code widget requires fixed size value. assuming two flex columns split the screen
|
||||
// at 50%, the appsel dropdown's content on the left should be the same as the desired
|
||||
// QR code size on the right.
|
||||
wincont.recalculateLayout(); // ensure contentWidth returns correct value
|
||||
const qr = try lvgl.QrCode.new(rightcol, appsel.contentWidth(), null);
|
||||
const qrerr = try lvgl.Label.new(rightcol, "QR data too large to display", .{});
|
||||
qrerr.hide();
|
||||
|
||||
// pairing struct must be set last, past all try/catch err.
|
||||
// otherwise, errdefer will double free urlmap: once in here, second in tab.destroySetup.
|
||||
tab.seed_setup.?.pairing = .{
|
||||
.urlmap = urlmap,
|
||||
.appsel = appsel,
|
||||
.appdesc = appdesc,
|
||||
.qr = qr,
|
||||
.qrerr = qrerr,
|
||||
};
|
||||
updatePairingApp();
|
||||
}
|
||||
|
||||
export fn nm_lnd_setup_appsel_changed(_: *lvgl.LvEvent) void {
|
||||
updatePairingApp();
|
||||
}
|
||||
|
||||
fn updatePairingApp() void {
|
||||
const pairing = tab.seed_setup.?.pairing.?;
|
||||
var buf: [128]u8 = undefined;
|
||||
const appname = pairing.appsel.getSelectedStr(&buf);
|
||||
if (app_description.get(appname)) |appdesc| {
|
||||
pairing.appdesc.setTextStatic(appdesc);
|
||||
pairing.qr.show();
|
||||
pairing.qrerr.hide();
|
||||
pairing.qr.setQrData(pairing.urlmap.get(appname).?) catch |err| {
|
||||
logger.err("updatePairingApp: setQrData: {!}", .{err});
|
||||
pairing.qr.hide();
|
||||
pairing.qrerr.show();
|
||||
};
|
||||
} else {
|
||||
logger.err("updatePairingApp: unknown app name [{s}]", .{appname});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
fn promptNodeReset() !void {
|
||||
const proceed: [*:0]const u8 = "PROCEED"; // btn idx 0
|
||||
const abort: [*:0]const u8 = "CANCEL"; // btn idx 1
|
||||
const title = " " ++ symbol.Warning ++ " LIGHTNING NODE RESET";
|
||||
const text =
|
||||
\\ARE YOU SURE?
|
||||
\\
|
||||
\\once reset, all funds managed by this node become
|
||||
\\permanently inaccessible unless a copy of the seed
|
||||
\\is available.
|
||||
\\
|
||||
\\the mnemonic seed allows restoring access to the
|
||||
\\on-chain portion of the funds.
|
||||
;
|
||||
widget.modal(title, text, &.{ proceed, abort }, nodeResetModalCallback) catch |err| {
|
||||
logger.err("promptNodeReset: modal: {any}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn nodeResetModalCallback(btn_idx: usize) align(@alignOf(widget.ModalButtonCallbackFn)) void {
|
||||
defer preserve_main_active_tab();
|
||||
// proceed = 0, cancel = 1
|
||||
if (btn_idx != 0) {
|
||||
return;
|
||||
}
|
||||
comm.pipeWrite(.lightning_reset) catch |err| {
|
||||
logger.err("nodeResetModalCallback: failed to request node reset: {!}", .{err});
|
||||
return;
|
||||
};
|
||||
tab.setMode(.startup);
|
||||
}
|
||||
|
||||
/// updates the tab when in regular operational mode.
|
||||
fn updateReport(rep: comm.Message.LightningReport) !void {
|
||||
var buf: [512]u8 = undefined;
|
||||
|
||||
// info section
|
||||
|
@ -132,9 +538,9 @@ pub fn updateTabPanel(rep: comm.Message.LightningReport) !void {
|
|||
});
|
||||
|
||||
// channels section
|
||||
tab.channels_cont.deleteChildren();
|
||||
tab.channels.cont.deleteChildren();
|
||||
for (rep.channels) |ch| {
|
||||
const chbox = (try lvgl.Container.new(tab.channels_cont)).flex(.column, .{});
|
||||
const chbox = (try lvgl.Container.new(tab.channels.cont)).flex(.column, .{});
|
||||
chbox.setWidth(lvgl.sizePercent(100));
|
||||
chbox.setHeightToContent();
|
||||
_ = try switch (ch.state) {
|
||||
|
|
|
@ -162,6 +162,10 @@ pub const LvEvent = opaque {
|
|||
pub fn userdata(self: *LvEvent) ?*anyopaque {
|
||||
return lv_event_get_user_data(self);
|
||||
}
|
||||
|
||||
pub fn stopBubbling(self: *LvEvent) void {
|
||||
lv_event_stop_bubbling(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// represents lv_disp_t in C.
|
||||
|
@ -350,6 +354,12 @@ pub const BaseObjMethods = struct {
|
|||
nm_obj_set_userdata(self.lvobj, data);
|
||||
}
|
||||
|
||||
/// updates layout of all children so that functions like `WidgetMethods.contentWidth`
|
||||
/// return correct results, when done in a single LVGL loop iteration.
|
||||
pub fn recalculateLayout(self: anytype) void {
|
||||
lv_obj_update_layout(self.lvobj);
|
||||
}
|
||||
|
||||
/// creates a new event handler where cb is called upon event with the filter code.
|
||||
/// to make cb called on any event, use EventCode.all filter.
|
||||
/// multiple event handlers are called in the same order as they were added.
|
||||
|
@ -361,6 +371,10 @@ pub const BaseObjMethods = struct {
|
|||
|
||||
/// methods applicable to visible objects like labels, buttons and containers.
|
||||
pub const WidgetMethods = struct {
|
||||
pub fn contentWidth(self: anytype) Coord {
|
||||
return lv_obj_get_content_width(self.lvobj);
|
||||
}
|
||||
|
||||
/// sets object horizontal length.
|
||||
pub fn setWidth(self: anytype, val: Coord) void {
|
||||
lv_obj_set_width(self.lvobj, val);
|
||||
|
@ -553,6 +567,8 @@ pub const FlexLayout = struct {
|
|||
cross: AlignCross = .start,
|
||||
track: Align = .start,
|
||||
all: ?Align = null, // overrides all 3 above
|
||||
width: ?Coord = null,
|
||||
height: ?union(enum) { fixed: Coord, content } = null,
|
||||
};
|
||||
|
||||
/// creates a new object with flex layout and some default padding.
|
||||
|
@ -563,6 +579,15 @@ pub const FlexLayout = struct {
|
|||
lv_obj_remove_style(obj, null, bgsel.value());
|
||||
const flex = adopt(obj, flow, opt);
|
||||
flex.padColumnDefault();
|
||||
if (opt.width) |w| {
|
||||
flex.setWidth(w);
|
||||
}
|
||||
if (opt.height) |h| {
|
||||
switch (h) {
|
||||
.content => flex.setHeightToContent(),
|
||||
.fixed => |v| flex.setHeight(v),
|
||||
}
|
||||
}
|
||||
return flex;
|
||||
}
|
||||
|
||||
|
@ -849,7 +874,7 @@ pub const Dropdown = struct {
|
|||
}
|
||||
|
||||
/// returns selected option as a slice of the buf.
|
||||
/// LVGL's lv_dropdown drawing supports up to 128 chars.
|
||||
/// LVGL's lv_dropdown supports up to 128 chars.
|
||||
pub fn getSelectedStr(self: Dropdown, buf: []u8) [:0]const u8 {
|
||||
const buflen: u32 = @min(buf.len, std.math.maxInt(u32));
|
||||
lv_dropdown_get_selected_str(self.lvobj, buf.ptr, buflen);
|
||||
|
@ -860,6 +885,34 @@ pub const Dropdown = struct {
|
|||
}
|
||||
};
|
||||
|
||||
pub const QrCode = struct {
|
||||
lvobj: *LvObj,
|
||||
|
||||
pub usingnamespace BaseObjMethods;
|
||||
pub usingnamespace WidgetMethods;
|
||||
|
||||
pub fn new(parent: anytype, size: Coord, data: ?[]const u8) !QrCode {
|
||||
const o = lv_qrcode_create(parent.lvobj, size, Black, White) orelse return error.OutOfMemory;
|
||||
const q = QrCode{ .lvobj = o };
|
||||
errdefer q.destroy();
|
||||
if (data) |d| {
|
||||
try q.setQrData(d);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
pub fn setQrData(self: QrCode, data: []const u8) !void {
|
||||
if (data.len > std.math.maxInt(u32)) {
|
||||
return error.QrCodeDataTooLarge;
|
||||
}
|
||||
const len: u32 = @truncate(data.len);
|
||||
const res = lv_qrcode_update(self.lvobj, data.ptr, len);
|
||||
if (res != c.LV_RES_OK) {
|
||||
return error.QrCodeSetData;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// represents lv_obj_t type in C.
|
||||
pub const LvObj = opaque {
|
||||
/// feature-flags controlling object's behavior.
|
||||
|
@ -1027,6 +1080,7 @@ extern fn lv_event_get_code(e: *LvEvent) LvEvent.Code;
|
|||
extern fn lv_event_get_current_target(e: *LvEvent) *LvObj;
|
||||
extern fn lv_event_get_target(e: *LvEvent) *LvObj;
|
||||
extern fn lv_event_get_user_data(e: *LvEvent) ?*anyopaque;
|
||||
extern fn lv_event_stop_bubbling(e: *LvEvent) void;
|
||||
extern fn lv_obj_add_event_cb(obj: *LvObj, cb: LvEvent.Callback, filter: LvEvent.Code, userdata: ?*anyopaque) *LvEvent.Descriptor;
|
||||
|
||||
// display and screen functions ----------------------------------------------
|
||||
|
@ -1085,6 +1139,8 @@ extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj;
|
|||
extern fn lv_obj_del(obj: *LvObj) void;
|
||||
/// deletes children of the obj.
|
||||
extern fn lv_obj_clean(obj: *LvObj) void;
|
||||
/// recalculates an object layout based on all its children.
|
||||
pub extern fn lv_obj_update_layout(obj: *const LvObj) void;
|
||||
|
||||
extern fn lv_obj_add_state(obj: *LvObj, c.lv_state_t) void;
|
||||
extern fn lv_obj_clear_state(obj: *LvObj, c.lv_state_t) void;
|
||||
|
@ -1096,6 +1152,7 @@ extern fn lv_obj_align(obj: *LvObj, a: c.lv_align_t, x: c.lv_coord_t, y: c.lv_co
|
|||
extern fn lv_obj_set_height(obj: *LvObj, h: c.lv_coord_t) void;
|
||||
extern fn lv_obj_set_width(obj: *LvObj, w: c.lv_coord_t) void;
|
||||
extern fn lv_obj_set_size(obj: *LvObj, w: c.lv_coord_t, h: c.lv_coord_t) void;
|
||||
extern fn lv_obj_get_content_width(obj: *const LvObj) c.lv_coord_t;
|
||||
|
||||
extern fn lv_obj_set_flex_flow(obj: *LvObj, flow: c.lv_flex_flow_t) void;
|
||||
extern fn lv_obj_set_flex_grow(obj: *LvObj, val: u8) void;
|
||||
|
@ -1134,3 +1191,6 @@ extern fn lv_bar_set_range(bar: *LvObj, min: i32, max: i32) void;
|
|||
extern fn lv_win_create(parent: *LvObj, header_height: c.lv_coord_t) ?*LvObj;
|
||||
extern fn lv_win_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj;
|
||||
extern fn lv_win_get_content(win: *LvObj) *LvObj;
|
||||
|
||||
extern fn lv_qrcode_create(parent: *LvObj, size: c.lv_coord_t, dark: Color, light: Color) ?*LvObj;
|
||||
extern fn lv_qrcode_update(qrcode: *LvObj, data: *const anyopaque, data_len: u32) c.lv_res_t;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
///! see lv_symbols_def.h
|
||||
pub const LightningBolt = &[_]u8{ 0xef, 0x83, 0xa7 };
|
||||
pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 };
|
||||
pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c };
|
||||
pub const Power = &[_]u8{ 0xef, 0x80, 0x91 };
|
||||
pub const Right = &[_]u8{ 0xef, 0x81, 0x94 };
|
||||
pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 };
|
||||
|
|
|
@ -14,9 +14,16 @@ pub const settings = @import("settings.zig");
|
|||
|
||||
const logger = std.log.scoped(.ui);
|
||||
|
||||
// defined in src/ui/c/ui.c
|
||||
// calls back into nm_create_xxx_panel functions defined here during init.
|
||||
extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int;
|
||||
|
||||
pub fn init() !void {
|
||||
// global allocator set on init.
|
||||
// must be set before a call to nm_ui_init.
|
||||
var allocator: std.mem.Allocator = undefined;
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator) !void {
|
||||
allocator = gpa;
|
||||
lvgl.init();
|
||||
const disp = try drv.initDisplay();
|
||||
drv.initInput() catch |err| {
|
||||
|
@ -47,7 +54,7 @@ export fn nm_create_bitcoin_panel(parent: *lvgl.LvObj) c_int {
|
|||
}
|
||||
|
||||
export fn nm_create_lightning_panel(parent: *lvgl.LvObj) c_int {
|
||||
lightning.initTabPanel(lvgl.Container{ .lvobj = parent }) catch |err| {
|
||||
lightning.initTabPanel(allocator, lvgl.Container{ .lvobj = parent }) catch |err| {
|
||||
logger.err("createLightningPanel: {any}", .{err});
|
||||
return -1;
|
||||
};
|
||||
|
|
Reference in New Issue