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,
|
onchain_report = 0x0a,
|
||||||
// nd -> ngui: lnd status and stats report
|
// nd -> ngui: lnd status and stats report
|
||||||
lightning_report = 0x0b,
|
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
|
// ngui -> nd: switch sysupdates channel
|
||||||
switch_sysupdates = 0x0c,
|
switch_sysupdates = 0x0c,
|
||||||
// nd -> ngui: all ndg settings
|
// nd -> ngui: all ndg settings
|
||||||
settings = 0x0d,
|
settings = 0x0d,
|
||||||
// next: 0x0e
|
// next: 0x15
|
||||||
};
|
};
|
||||||
|
|
||||||
/// daemon and gui exchange messages of this type.
|
/// daemon and gui exchange messages of this type.
|
||||||
|
@ -89,6 +103,13 @@ pub const Message = union(MessageTag) {
|
||||||
poweroff_progress: PoweroffProgress,
|
poweroff_progress: PoweroffProgress,
|
||||||
onchain_report: OnchainReport,
|
onchain_report: OnchainReport,
|
||||||
lightning_report: LightningReport,
|
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,
|
switch_sysupdates: SysupdatesChan,
|
||||||
settings: Settings,
|
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 {
|
pub const SysupdatesChan = enum {
|
||||||
stable, // master branch in sysupdates
|
stable, // master branch in sysupdates
|
||||||
edge, // dev 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);
|
const len = try reader.readIntLittle(u64);
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
return switch (tag) {
|
return switch (tag) {
|
||||||
|
.lightning_get_ctrlconn => .{ .value = .lightning_get_ctrlconn },
|
||||||
|
.lightning_reset => .{ .value = .lightning_reset },
|
||||||
.ping => .{ .value = .{ .ping = {} } },
|
.ping => .{ .value = .{ .ping = {} } },
|
||||||
.pong => .{ .value = .{ .pong = {} } },
|
.pong => .{ .value = .{ .pong = {} } },
|
||||||
.poweroff => .{ .value = .{ .poweroff = {} } },
|
.poweroff => .{ .value = .{ .poweroff = {} } },
|
||||||
|
@ -235,7 +290,14 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
switch (tag) {
|
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| {
|
inline else => |t| {
|
||||||
var bytes = try allocator.alloc(u8, len);
|
var bytes = try allocator.alloc(u8, len);
|
||||||
defer allocator.free(bytes);
|
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()),
|
.poweroff_progress => try json.stringify(msg.poweroff_progress, .{}, data.writer()),
|
||||||
.onchain_report => try json.stringify(msg.onchain_report, .{}, data.writer()),
|
.onchain_report => try json.stringify(msg.onchain_report, .{}, data.writer()),
|
||||||
.lightning_report => try json.stringify(msg.lightning_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()),
|
.switch_sysupdates => try json.stringify(msg.switch_sysupdates, .{}, data.writer()),
|
||||||
.settings => try json.stringify(msg.settings, .{}, data.writer()),
|
.settings => try json.stringify(msg.settings, .{}, data.writer()),
|
||||||
}
|
}
|
||||||
|
@ -364,6 +433,8 @@ test "write/read void tags" {
|
||||||
defer buf.deinit();
|
defer buf.deinit();
|
||||||
|
|
||||||
const msg = [_]Message{
|
const msg = [_]Message{
|
||||||
|
Message.lightning_get_ctrlconn,
|
||||||
|
Message.lightning_reset,
|
||||||
Message.ping,
|
Message.ping,
|
||||||
Message.pong,
|
Message.pong,
|
||||||
Message.poweroff,
|
Message.poweroff,
|
||||||
|
|
154
src/lndhttp.zig
154
src/lndhttp.zig
|
@ -1,6 +1,8 @@
|
||||||
//! lnd lightning HTTP client and utility functions.
|
//! lnd lightning HTTP client and utility functions.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const base64enc = std.base64.standard.Encoder;
|
||||||
|
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
|
|
||||||
/// safe for concurrent use as long as Client.allocator is.
|
/// safe for concurrent use as long as Client.allocator is.
|
||||||
|
@ -10,30 +12,43 @@ pub const Client = struct {
|
||||||
port: u16 = 10010,
|
port: u16 = 10010,
|
||||||
apibase: []const u8, // https://localhost:10010
|
apibase: []const u8, // https://localhost:10010
|
||||||
macaroon: struct {
|
macaroon: struct {
|
||||||
readonly: []const u8,
|
readonly: ?[]const u8,
|
||||||
admin: ?[]const u8,
|
admin: ?[]const u8,
|
||||||
},
|
},
|
||||||
httpClient: std.http.Client,
|
httpClient: std.http.Client,
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
LndHttpMissingMacaroon,
|
||||||
|
LndHttpBadStatusCode,
|
||||||
|
LndPayloadWriteFail,
|
||||||
|
};
|
||||||
|
|
||||||
pub const ApiMethod = enum {
|
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
|
feereport, // fees of all active channels
|
||||||
getinfo, // general host node info
|
getinfo, // general host node info
|
||||||
getnetworkinfo, // visible graph info
|
getnetworkinfo, // visible graph info
|
||||||
listchannels, // active channels
|
listchannels, // active channels
|
||||||
pendingchannels, // pending open/close channels
|
pendingchannels, // pending open/close channels
|
||||||
walletbalance, // onchain balance
|
walletbalance, // onchain balance
|
||||||
walletstatus, // server/wallet status
|
|
||||||
// fwdinghistory, getchaninfo, getnodeinfo
|
// fwdinghistory, getchaninfo, getnodeinfo
|
||||||
// genseed, initwallet, unlockwallet
|
|
||||||
// watchtower: getinfo, stats, list, add, remove
|
// watchtower: getinfo, stats, list, add, remove
|
||||||
|
|
||||||
fn apipath(self: @This()) []const u8 {
|
fn apipath(self: @This()) []const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.feereport => "v1/fees",
|
.feereport => "v1/fees",
|
||||||
|
.genseed => "v1/genseed",
|
||||||
.getinfo => "v1/getinfo",
|
.getinfo => "v1/getinfo",
|
||||||
.getnetworkinfo => "v1/graph/info",
|
.getnetworkinfo => "v1/graph/info",
|
||||||
|
.initwallet => "v1/initwallet",
|
||||||
.listchannels => "v1/channels",
|
.listchannels => "v1/channels",
|
||||||
.pendingchannels => "v1/channels/pending",
|
.pendingchannels => "v1/channels/pending",
|
||||||
|
.unlockwallet => "v1/unlockwallet",
|
||||||
.walletbalance => "v1/balance/blockchain",
|
.walletbalance => "v1/balance/blockchain",
|
||||||
.walletstatus => "v1/state",
|
.walletstatus => "v1/state",
|
||||||
};
|
};
|
||||||
|
@ -42,6 +57,20 @@ pub const Client = struct {
|
||||||
|
|
||||||
pub fn MethodArgs(comptime m: ApiMethod) type {
|
pub fn MethodArgs(comptime m: ApiMethod) type {
|
||||||
return switch (m) {
|
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 {
|
.listchannels => struct {
|
||||||
status: ?enum { active, inactive } = null,
|
status: ?enum { active, inactive } = null,
|
||||||
advert: ?enum { public, private } = null,
|
advert: ?enum { public, private } = null,
|
||||||
|
@ -55,10 +84,13 @@ pub const Client = struct {
|
||||||
pub fn ResultValue(comptime m: ApiMethod) type {
|
pub fn ResultValue(comptime m: ApiMethod) type {
|
||||||
return switch (m) {
|
return switch (m) {
|
||||||
.feereport => FeeReport,
|
.feereport => FeeReport,
|
||||||
|
.genseed => GeneratedSeed,
|
||||||
.getinfo => LndInfo,
|
.getinfo => LndInfo,
|
||||||
.getnetworkinfo => NetworkInfo,
|
.getnetworkinfo => NetworkInfo,
|
||||||
|
.initwallet => InitedWallet,
|
||||||
.listchannels => ChannelsList,
|
.listchannels => ChannelsList,
|
||||||
.pendingchannels => PendingList,
|
.pendingchannels => PendingList,
|
||||||
|
.unlockwallet => struct {},
|
||||||
.walletbalance => WalletBalance,
|
.walletbalance => WalletBalance,
|
||||||
.walletstatus => WalletStatus,
|
.walletstatus => WalletStatus,
|
||||||
};
|
};
|
||||||
|
@ -69,7 +101,7 @@ pub const Client = struct {
|
||||||
hostname: []const u8 = "localhost", // must be present in tlscert_path SANs
|
hostname: []const u8 = "localhost", // must be present in tlscert_path SANs
|
||||||
port: u16 = 10010, // HTTP API port
|
port: u16 = 10010, // HTTP API port
|
||||||
tlscert_path: []const u8, // must contain the hostname in SANs
|
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
|
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.
|
/// must deinit when done.
|
||||||
pub fn init(opt: InitOpt) !Client {
|
pub fn init(opt: InitOpt) !Client {
|
||||||
var ca = std.crypto.Certificate.Bundle{}; // deinit'ed by http.Client.deinit
|
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);
|
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 });
|
const apibase = try std.fmt.allocPrint(opt.allocator, "https://{s}:{d}", .{ opt.hostname, opt.port });
|
||||||
errdefer opt.allocator.free(apibase);
|
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 .{
|
return .{
|
||||||
.allocator = opt.allocator,
|
.allocator = opt.allocator,
|
||||||
.apibase = apibase,
|
.apibase = apibase,
|
||||||
|
@ -99,10 +132,8 @@ pub const Client = struct {
|
||||||
pub fn deinit(self: *Client) void {
|
pub fn deinit(self: *Client) void {
|
||||||
self.httpClient.deinit();
|
self.httpClient.deinit();
|
||||||
self.allocator.free(self.apibase);
|
self.allocator.free(self.apibase);
|
||||||
self.allocator.free(self.macaroon.readonly);
|
if (self.macaroon.readonly) |ro| self.allocator.free(ro);
|
||||||
if (self.macaroon.admin) |a| {
|
if (self.macaroon.admin) |a| self.allocator.free(a);
|
||||||
self.allocator.free(a);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn Result(comptime m: ApiMethod) type {
|
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
|
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);
|
var req = try self.httpClient.request(reqinfo.httpmethod, reqinfo.url, reqinfo.headers, opt);
|
||||||
defer req.deinit();
|
defer req.deinit();
|
||||||
|
if (reqinfo.payload) |p| {
|
||||||
|
req.transfer_encoding = .{ .content_length = p.len };
|
||||||
|
}
|
||||||
|
|
||||||
try req.start();
|
try req.start();
|
||||||
if (reqinfo.payload) |p| {
|
if (reqinfo.payload) |p| {
|
||||||
try req.writer().writeAll(p);
|
req.writer().writeAll(p) catch return Error.LndPayloadWriteFail;
|
||||||
try req.finish();
|
try req.finish();
|
||||||
}
|
}
|
||||||
try req.wait();
|
try req.wait();
|
||||||
if (req.response.status.class() != .success) {
|
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) {
|
if (@TypeOf(Result(apimethod)) == void) {
|
||||||
return; // void response; need no json parsing
|
return; // void response; need no json parsing
|
||||||
|
@ -153,12 +191,61 @@ pub const Client = struct {
|
||||||
errdefer reqinfo.deinit();
|
errdefer reqinfo.deinit();
|
||||||
const arena = reqinfo.arena.allocator();
|
const arena = reqinfo.arena.allocator();
|
||||||
reqinfo.value = switch (apimethod) {
|
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,
|
.httpmethod = .GET,
|
||||||
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
|
||||||
.headers = blk: {
|
.headers = blk: {
|
||||||
|
if (self.macaroon.readonly == null) {
|
||||||
|
return Error.LndHttpMissingMacaroon;
|
||||||
|
}
|
||||||
var h = std.http.Headers{ .allocator = arena };
|
var h = std.http.Headers{ .allocator = arena };
|
||||||
try h.append(authHeaderName, self.macaroon.readonly);
|
try h.append(authHeaderName, self.macaroon.readonly.?);
|
||||||
break :blk h;
|
break :blk h;
|
||||||
},
|
},
|
||||||
.payload = null,
|
.payload = null,
|
||||||
|
@ -184,8 +271,11 @@ pub const Client = struct {
|
||||||
break :blk try std.Uri.parse(buf.items); // uri point to the original buf
|
break :blk try std.Uri.parse(buf.items); // uri point to the original buf
|
||||||
},
|
},
|
||||||
.headers = blk: {
|
.headers = blk: {
|
||||||
|
if (self.macaroon.readonly == null) {
|
||||||
|
return Error.LndHttpMissingMacaroon;
|
||||||
|
}
|
||||||
var h = std.http.Headers{ .allocator = arena };
|
var h = std.http.Headers{ .allocator = arena };
|
||||||
try h.append(authHeaderName, self.macaroon.readonly);
|
try h.append(authHeaderName, self.macaroon.readonly.?);
|
||||||
break :blk h;
|
break :blk h;
|
||||||
},
|
},
|
||||||
.payload = null,
|
.payload = null,
|
||||||
|
@ -194,13 +284,23 @@ pub const Client = struct {
|
||||||
return reqinfo;
|
return reqinfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns null if file not found.
|
||||||
/// callers own returned value.
|
/// callers own returned value.
|
||||||
fn readMacaroon(gpa: std.mem.Allocator, path: []const u8) ![]const u8 {
|
fn readMacaroonOrNull(gpa: std.mem.Allocator, path: []const u8) !?[]const u8 {
|
||||||
const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only });
|
const file = std.fs.openFileAbsolute(path, .{ .mode = .read_only }) catch |err| switch (err) {
|
||||||
|
error.FileNotFound => return null,
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
defer file.close();
|
defer file.close();
|
||||||
const cont = try file.readToEndAlloc(gpa, 1024);
|
const raw = try file.readToEndAlloc(gpa, 1024);
|
||||||
defer gpa.free(cont);
|
defer gpa.free(raw);
|
||||||
return std.fmt.allocPrint(gpa, "{}", .{std.fmt.fmtSliceHexLower(cont)});
|
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,
|
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_NAME = "update.sh";
|
||||||
const SYSUPDATES_RUN_SCRIPT_PATH = "/ssd/sysupdates/" ++ SYSUPDATES_RUN_SCRIPT_NAME;
|
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
|
arena: *std.heap.ArenaAllocator, // data is allocated here
|
||||||
confpath: []const u8, // fs path to where data is persisted
|
confpath: []const u8, // fs path to where data is persisted
|
||||||
|
|
||||||
|
static: StaticData,
|
||||||
mu: std.Thread.RwLock = .{},
|
mu: std.Thread.RwLock = .{},
|
||||||
data: Data,
|
data: Data,
|
||||||
|
|
||||||
|
@ -25,6 +41,12 @@ pub const Data = struct {
|
||||||
sysrunscript: []const u8,
|
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.
|
/// enums must match git branches in https://git.qcode.ch/nakamochi/sysupdates.
|
||||||
pub const SysupdatesChannel = enum {
|
pub const SysupdatesChannel = enum {
|
||||||
master, // stable
|
master, // stable
|
||||||
|
@ -43,8 +65,9 @@ pub fn init(allocator: std.mem.Allocator, confpath: []const u8) !Config {
|
||||||
}
|
}
|
||||||
return .{
|
return .{
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.data = try initData(arena.allocator(), confpath),
|
|
||||||
.confpath = 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;
|
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.
|
/// 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.? {
|
pub fn safeReadOnly(self: *Config, comptime F: anytype) @typeInfo(@TypeOf(F)).Fn.return_type.? {
|
||||||
self.mu.lockShared();
|
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" {
|
test "init existing" {
|
||||||
const t = std.testing;
|
const t = std.testing;
|
||||||
const tt = @import("../test.zig");
|
const tt = @import("../test.zig");
|
||||||
|
@ -220,6 +404,7 @@ test "dump" {
|
||||||
.syscronscript = "cronscript.sh",
|
.syscronscript = "cronscript.sh",
|
||||||
.sysrunscript = "runscript.sh",
|
.sysrunscript = "runscript.sh",
|
||||||
},
|
},
|
||||||
|
.static = undefined,
|
||||||
};
|
};
|
||||||
// purposefully skip conf.deinit() - expecting no leaking allocations in conf.dump.
|
// purposefully skip conf.deinit() - expecting no leaking allocations in conf.dump.
|
||||||
try conf.dump();
|
try conf.dump();
|
||||||
|
@ -252,6 +437,7 @@ test "switch sysupdates and infer" {
|
||||||
.syscronscript = cronscript,
|
.syscronscript = cronscript,
|
||||||
.sysrunscript = SYSUPDATES_RUN_SCRIPT_PATH,
|
.sysrunscript = SYSUPDATES_RUN_SCRIPT_PATH,
|
||||||
},
|
},
|
||||||
|
.static = undefined,
|
||||||
};
|
};
|
||||||
// purposefully skip conf.deinit() - expecting no leaking allocations.
|
// purposefully skip conf.deinit() - expecting no leaking allocations.
|
||||||
|
|
||||||
|
@ -290,6 +476,7 @@ test "switch sysupdates with .run=true" {
|
||||||
.syscronscript = try tmp.join(&.{"cronscript.sh"}),
|
.syscronscript = try tmp.join(&.{"cronscript.sh"}),
|
||||||
.sysrunscript = try tmp.join(&.{runscript}),
|
.sysrunscript = try tmp.join(&.{runscript}),
|
||||||
},
|
},
|
||||||
|
.static = undefined,
|
||||||
};
|
};
|
||||||
defer conf.deinit();
|
defer conf.deinit();
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ state: enum {
|
||||||
running,
|
running,
|
||||||
standby,
|
standby,
|
||||||
poweroff,
|
poweroff,
|
||||||
|
wallet_reset,
|
||||||
},
|
},
|
||||||
|
|
||||||
main_thread: ?std.Thread = null,
|
main_thread: ?std.Thread = null,
|
||||||
|
@ -64,14 +65,56 @@ onchain_report_interval: u64 = 1 * time.ns_per_min,
|
||||||
want_lnd_report: bool,
|
want_lnd_report: bool,
|
||||||
lnd_timer: time.Timer,
|
lnd_timer: time.Timer,
|
||||||
lnd_report_interval: u64 = 1 * time.ns_per_min,
|
lnd_report_interval: u64 = 1 * time.ns_per_min,
|
||||||
|
lnd_tls_reset_count: usize = 0,
|
||||||
|
|
||||||
/// system services actively managed by the daemon.
|
/// system services actively managed by the daemon.
|
||||||
/// these are stop'ed during poweroff and their shutdown progress sent to ngui.
|
/// 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.
|
/// 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 Daemon = @This();
|
||||||
|
|
||||||
|
const Error = error{
|
||||||
|
InvalidState,
|
||||||
|
WalletResetActive,
|
||||||
|
PoweroffActive,
|
||||||
|
AlreadyStarted,
|
||||||
|
ConnectWifiEmptySSID,
|
||||||
|
MakeWalletUnlockFileFail,
|
||||||
|
LndServiceStopFail,
|
||||||
|
ResetLndFail,
|
||||||
|
GenLndConfigFail,
|
||||||
|
InitLndWallet,
|
||||||
|
UnlockLndWallet,
|
||||||
|
};
|
||||||
|
|
||||||
const InitOpt = struct {
|
const InitOpt = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
confpath: []const u8,
|
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
|
// the order is important. when powering off, the services are shut down
|
||||||
// in the same order appended here.
|
// 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, SysService.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.BITCOIND, .{ .stop_wait_sec = 600 }));
|
||||||
|
|
||||||
const conf = try Config.init(opt.allocator, opt.confpath);
|
const conf = try Config.init(opt.allocator, opt.confpath);
|
||||||
errdefer conf.deinit();
|
errdefer conf.deinit();
|
||||||
|
@ -103,7 +146,7 @@ pub fn init(opt: InitOpt) !Daemon {
|
||||||
.uiwriter = opt.uiw,
|
.uiwriter = opt.uiw,
|
||||||
.wpa_ctrl = try types.WpaControl.open(opt.wpa),
|
.wpa_ctrl = try types.WpaControl.open(opt.wpa),
|
||||||
.state = .stopped,
|
.state = .stopped,
|
||||||
.services = try svlist.toOwnedSlice(),
|
.services = .{ .list = try svlist.toOwnedSlice() },
|
||||||
// send persisted settings immediately on start
|
// send persisted settings immediately on start
|
||||||
.want_settings = true,
|
.want_settings = true,
|
||||||
// send a network report right at start without wifi scan to make it faster.
|
// 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.
|
/// releases all associated resources.
|
||||||
/// the daemon must be stop'ed and wait'ed before deiniting.
|
/// the daemon must be stop'ed and wait'ed before deiniting.
|
||||||
pub fn deinit(self: *Daemon) void {
|
pub fn deinit(self: *Daemon) void {
|
||||||
defer self.conf.deinit();
|
|
||||||
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
|
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
|
||||||
for (self.services) |*sv| {
|
self.services.deinit(self.allocator);
|
||||||
sv.deinit();
|
self.conf.deinit();
|
||||||
}
|
|
||||||
self.allocator.free(self.services);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// start launches daemon threads and returns immediately.
|
/// start launches daemon threads and returns immediately.
|
||||||
|
@ -138,8 +178,8 @@ pub fn start(self: *Daemon) !void {
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.stopped => {}, // continue
|
.stopped => {}, // continue
|
||||||
.poweroff => return error.InPoweroffState,
|
.poweroff => return Error.PoweroffActive,
|
||||||
else => return error.AlreadyStarted,
|
else => return Error.AlreadyStarted,
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.wpa_ctrl.attach();
|
try self.wpa_ctrl.attach();
|
||||||
|
@ -193,7 +233,8 @@ fn standby(self: *Daemon) !void {
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.standby => {},
|
.standby => {},
|
||||||
.stopped, .poweroff => return error.InvalidState,
|
.stopped, .poweroff => return Error.InvalidState,
|
||||||
|
.wallet_reset => return Error.WalletResetActive,
|
||||||
.running => {
|
.running => {
|
||||||
try screen.backlight(.off);
|
try screen.backlight(.off);
|
||||||
self.state = .standby;
|
self.state = .standby;
|
||||||
|
@ -206,8 +247,8 @@ fn wakeup(self: *Daemon) !void {
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.running => {},
|
.running, .wallet_reset => {},
|
||||||
.stopped, .poweroff => return error.InvalidState,
|
.stopped, .poweroff => return Error.InvalidState,
|
||||||
.standby => {
|
.standby => {
|
||||||
try screen.backlight(.on);
|
try screen.backlight(.on);
|
||||||
self.state = .running;
|
self.state = .running;
|
||||||
|
@ -225,7 +266,8 @@ fn beginPoweroff(self: *Daemon) !void {
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.poweroff => {}, // already in poweroff mode
|
.poweroff => {}, // already in poweroff mode
|
||||||
.stopped => return error.InvalidState,
|
.stopped => return Error.InvalidState,
|
||||||
|
.wallet_reset => return Error.WalletResetActive,
|
||||||
.running, .standby => {
|
.running, .standby => {
|
||||||
self.poweroff_thread = try std.Thread.spawn(.{}, poweroffThread, .{self});
|
self.poweroff_thread = try std.Thread.spawn(.{}, poweroffThread, .{self});
|
||||||
self.state = .poweroff;
|
self.state = .poweroff;
|
||||||
|
@ -250,13 +292,13 @@ fn poweroffThread(self: *Daemon) void {
|
||||||
};
|
};
|
||||||
|
|
||||||
// initiate shutdown of all services concurrently.
|
// 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 });
|
sv.stop() catch |err| logger.err("sv stop '{s}': {any}", .{ sv.name, err });
|
||||||
}
|
}
|
||||||
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
||||||
|
|
||||||
// wait each service until stopped or error.
|
// wait each service until stopped or error.
|
||||||
for (self.services) |*sv| {
|
for (self.services.list) |*sv| {
|
||||||
_ = sv.stopWait() catch {};
|
_ = sv.stopWait() catch {};
|
||||||
logger.info("{s} sv is now stopped; err={any}", .{ sv.name, sv.lastStopError() });
|
logger.info("{s} sv is now stopped; err={any}", .{ sv.name, sv.lastStopError() });
|
||||||
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
||||||
|
@ -311,6 +353,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
|
||||||
self.want_settings = !ok;
|
self.want_settings = !ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// network stats
|
||||||
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
||||||
if (self.want_wifi_scan) {
|
if (self.want_wifi_scan) {
|
||||||
if (self.startWifiScan()) {
|
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.want_onchain_report or self.bitcoin_timer.read() > self.onchain_report_interval) {
|
||||||
if (self.sendOnchainReport()) {
|
if (self.sendOnchainReport()) {
|
||||||
self.bitcoin_timer.reset();
|
self.bitcoin_timer.reset();
|
||||||
|
@ -335,12 +379,17 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
|
||||||
logger.err("sendOnchainReport: {any}", .{err});
|
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.want_lnd_report or self.lnd_timer.read() > self.lnd_report_interval) {
|
||||||
if (self.sendLightningReport()) {
|
if (self.sendLightningReport()) {
|
||||||
self.lnd_timer.reset();
|
self.lnd_timer.reset();
|
||||||
self.want_lnd_report = false;
|
self.want_lnd_report = false;
|
||||||
} else |err| {
|
} 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) {
|
switch (self.state) {
|
||||||
.stopped, .poweroff => break :loop,
|
.stopped, .poweroff => break :loop,
|
||||||
.running, .standby => {
|
.running, .standby, .wallet_reset => {
|
||||||
logger.err("commThreadLoop: {any}", .{err});
|
logger.err("commThreadLoop: {any}", .{err});
|
||||||
if (err == error.EndOfStream) {
|
if (err == error.EndOfStream) {
|
||||||
// pointless to continue running if comms I/O is broken.
|
// 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
|
// 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)}),
|
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.
|
/// sends poweroff progress to uiwriter in comm.Message.PoweroffProgress format.
|
||||||
fn sendPoweroffReport(self: *Daemon) !void {
|
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);
|
defer self.allocator.free(svstat);
|
||||||
for (self.services, svstat) |*sv, *stat| {
|
for (self.services.list, svstat) |*sv, *stat| {
|
||||||
stat.* = .{
|
stat.* = .{
|
||||||
.name = sv.name,
|
.name = sv.name,
|
||||||
.stopped = sv.status() == .stopped,
|
.stopped = sv.status() == .stopped,
|
||||||
|
@ -490,7 +560,7 @@ fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
||||||
/// initiates wifi connection procedure in a separate thread
|
/// initiates wifi connection procedure in a separate thread
|
||||||
fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !void {
|
fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !void {
|
||||||
if (ssid.len == 0) {
|
if (ssid.len == 0) {
|
||||||
return error.ConnectWifiEmptySSID;
|
return Error.ConnectWifiEmptySSID;
|
||||||
}
|
}
|
||||||
const ssid_copy = try self.allocator.dupe(u8, ssid);
|
const ssid_copy = try self.allocator.dupe(u8, ssid);
|
||||||
const pwd_copy = try self.allocator.dupe(u8, password);
|
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 {
|
fn sendOnchainReport(self: *Daemon) !void {
|
||||||
const stats = self.fetchOnchainStats() catch |err| {
|
const stats = self.fetchOnchainStats() catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
|
@ -631,6 +702,7 @@ const OnchainStats = struct {
|
||||||
balance: ?lndhttp.Client.Result(.walletbalance),
|
balance: ?lndhttp.Client.Result(.walletbalance),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// call site must hold self.mu due to self.state read access.
|
||||||
/// callers own returned value.
|
/// callers own returned value.
|
||||||
fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
||||||
var client = bitcoindrpc.Client{
|
var client = bitcoindrpc.Client{
|
||||||
|
@ -642,10 +714,14 @@ fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
||||||
const mempool = try client.call(.getmempoolinfo, {});
|
const mempool = try client.call(.getmempoolinfo, {});
|
||||||
|
|
||||||
const balance: ?lndhttp.Client.Result(.walletbalance) = blk: { // lndhttp.WalletBalance
|
const balance: ?lndhttp.Client.Result(.walletbalance) = blk: { // lndhttp.WalletBalance
|
||||||
|
if (self.state == .wallet_reset) {
|
||||||
|
break :blk null;
|
||||||
|
}
|
||||||
var lndc = lndhttp.Client.init(.{
|
var lndc = lndhttp.Client.init(.{
|
||||||
.allocator = self.allocator,
|
.allocator = self.allocator,
|
||||||
.tlscert_path = "/home/lnd/.lnd/tls.cert",
|
.tlscert_path = Config.LND_TLSCERT_PATH,
|
||||||
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
|
.macaroon_ro_path = Config.LND_MACAROON_RO_PATH,
|
||||||
|
.macaroon_admin_path = Config.LND_MACAROON_ADMIN_PATH,
|
||||||
}) catch break :blk null;
|
}) catch break :blk null;
|
||||||
defer lndc.deinit();
|
defer lndc.deinit();
|
||||||
const res = lndc.call(.walletbalance, {}) catch break :blk null;
|
const res = lndc.call(.walletbalance, {}) catch break :blk null;
|
||||||
|
@ -662,8 +738,9 @@ fn fetchOnchainStats(self: *Daemon) !OnchainStats {
|
||||||
fn sendLightningReport(self: *Daemon) !void {
|
fn sendLightningReport(self: *Daemon) !void {
|
||||||
var client = try lndhttp.Client.init(.{
|
var client = try lndhttp.Client.init(.{
|
||||||
.allocator = self.allocator,
|
.allocator = self.allocator,
|
||||||
.tlscert_path = "/home/lnd/.lnd/tls.cert",
|
.tlscert_path = Config.LND_TLSCERT_PATH,
|
||||||
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
|
.macaroon_ro_path = Config.LND_MACAROON_RO_PATH,
|
||||||
|
.macaroon_admin_path = Config.LND_MACAROON_ADMIN_PATH,
|
||||||
});
|
});
|
||||||
defer client.deinit();
|
defer client.deinit();
|
||||||
|
|
||||||
|
@ -801,6 +878,238 @@ fn sendLightningReport(self: *Daemon) !void {
|
||||||
try comm.write(self.allocator, self.uiwriter, .{ .lightning_report = lndrep });
|
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 {
|
fn switchSysupdates(self: *Daemon, chan: comm.Message.SysupdatesChan) !void {
|
||||||
const th = try std.Thread.spawn(.{}, switchSysupdatesThread, .{ self, chan });
|
const th = try std.Thread.spawn(.{}, switchSysupdatesThread, .{ self, chan });
|
||||||
th.detach();
|
th.detach();
|
||||||
|
@ -856,8 +1165,8 @@ test "start-stop" {
|
||||||
try t.expect(!daemon.wpa_ctrl.attached);
|
try t.expect(!daemon.wpa_ctrl.attached);
|
||||||
try t.expect(daemon.wpa_ctrl.opened);
|
try t.expect(daemon.wpa_ctrl.opened);
|
||||||
|
|
||||||
try t.expect(daemon.services.len > 0);
|
try t.expect(daemon.services.list.len > 0);
|
||||||
for (daemon.services) |*sv| {
|
for (daemon.services.list) |*sv| {
|
||||||
try t.expect(!sv.stop_proc.spawned);
|
try t.expect(!sv.stop_proc.spawned);
|
||||||
try t.expectEqual(SysService.Status.initial, sv.status());
|
try t.expectEqual(SysService.Status.initial, sv.status());
|
||||||
}
|
}
|
||||||
|
@ -902,7 +1211,7 @@ test "start-poweroff" {
|
||||||
daemon.wait();
|
daemon.wait();
|
||||||
try t.expect(daemon.state == .stopped);
|
try t.expect(daemon.state == .stopped);
|
||||||
try t.expect(daemon.poweroff_thread == null);
|
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.spawned);
|
||||||
try t.expect(sv.stop_proc.waited);
|
try t.expect(sv.stop_proc.waited);
|
||||||
try t.expectEqual(SysService.Status.stopped, sv.status());
|
try t.expectEqual(SysService.Status.stopped, sv.status());
|
||||||
|
|
|
@ -3,6 +3,18 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const types = @import("../types.zig");
|
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,
|
allocator: std.mem.Allocator,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
stop_wait_sec: ?u32 = null,
|
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
|
/// the .initial value is a temporary solution until service watcher and start
|
||||||
/// are implemnted: at the moment, SysService can only stop services, nothing else.
|
/// are implemnted: at the moment, SysService can only stop services, nothing else.
|
||||||
pub const Status = enum(u8) {
|
pub const Status = enum(u8) {
|
||||||
initial, // TODO: add .running
|
initial, // TODO: get rid of "initial" and infer the actual state
|
||||||
|
started,
|
||||||
stopping,
|
stopping,
|
||||||
stopped,
|
stopped,
|
||||||
};
|
};
|
||||||
|
|
||||||
const State = union(Status) {
|
const State = union(Status) {
|
||||||
initial: void,
|
initial: void,
|
||||||
|
started: std.ChildProcess.Term,
|
||||||
stopping: void,
|
stopping: void,
|
||||||
stopped: std.ChildProcess.Term,
|
stopped: std.ChildProcess.Term,
|
||||||
};
|
};
|
||||||
|
@ -64,6 +78,26 @@ pub fn lastStopError(self: *SysService) ?anyerror {
|
||||||
return self.stop_err;
|
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.
|
/// launches a service stop procedure and returns immediately.
|
||||||
/// callers must invoke stopWait to release all resources used by the stop.
|
/// callers must invoke stopWait to release all resources used by the stop.
|
||||||
pub fn stop(self: *SysService) !void {
|
pub fn stop(self: *SysService) !void {
|
||||||
|
@ -71,7 +105,7 @@ pub fn stop(self: *SysService) !void {
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
|
|
||||||
self.stop_err = null;
|
self.stop_err = null;
|
||||||
self.spawnStop() catch |err| {
|
self.spawnStopUnguarded() catch |err| {
|
||||||
self.stop_err = err;
|
self.stop_err = err;
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
@ -84,7 +118,7 @@ pub fn stopWait(self: *SysService) !void {
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
|
|
||||||
self.stop_err = null;
|
self.stop_err = null;
|
||||||
self.spawnStop() catch |err| {
|
self.spawnStopUnguarded() catch |err| {
|
||||||
self.stop_err = err;
|
self.stop_err = err;
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
@ -96,10 +130,10 @@ pub fn stopWait(self: *SysService) !void {
|
||||||
self.stat = .{ .stopped = term };
|
self.stat = .{ .stopped = term };
|
||||||
switch (term) {
|
switch (term) {
|
||||||
.Exited => |code| if (code != 0) {
|
.Exited => |code| if (code != 0) {
|
||||||
self.stop_err = error.SysServiceBadStopCode;
|
self.stop_err = Error.SysServiceBadStopCode;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
self.stop_err = error.SysServiceBadStopTerm;
|
self.stop_err = Error.SysServiceBadStopTerm;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (self.stop_err) |err| {
|
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.
|
/// actual internal body of SysService.stop: stopWait also uses this.
|
||||||
/// callers must hold self.mu.
|
/// callers must hold self.mu.
|
||||||
fn spawnStop(self: *SysService) !void {
|
fn spawnStopUnguarded(self: *SysService) !void {
|
||||||
switch (self.stat) {
|
switch (self.stat) {
|
||||||
.stopping => return, // already in progress
|
.stopping => return, // already in progress
|
||||||
// intentionally let .stopped state pass through: can't see any downsides.
|
// intentionally let .stopped state pass through: can't see any downsides.
|
||||||
.initial, .stopped => {},
|
.initial, .started, .stopped => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// use arena to simplify stop proc args construction.
|
// 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 = .{},
|
mu: std.Thread.Mutex = .{},
|
||||||
network: ?comm.ParsedMessage = null, // NetworkReport
|
network: ?comm.ParsedMessage = null, // NetworkReport
|
||||||
onchain: ?comm.ParsedMessage = null, // OnchainReport
|
onchain: ?comm.ParsedMessage = null, // OnchainReport
|
||||||
lightning: ?comm.ParsedMessage = null, // LightningReport
|
lightning: ?comm.ParsedMessage = null, // LightningReport or LightningError
|
||||||
|
|
||||||
fn deinit(self: *@This()) void {
|
fn deinit(self: *@This()) void {
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
|
@ -76,7 +76,7 @@ var last_report: struct {
|
||||||
}
|
}
|
||||||
self.onchain = new;
|
self.onchain = new;
|
||||||
},
|
},
|
||||||
.lightning_report => {
|
.lightning_report, .lightning_error => {
|
||||||
if (self.lightning) |old| {
|
if (self.lightning) |old| {
|
||||||
old.deinit();
|
old.deinit();
|
||||||
}
|
}
|
||||||
|
@ -233,7 +233,19 @@ fn commThreadLoopCycle() !void {
|
||||||
.network_report,
|
.network_report,
|
||||||
.onchain_report,
|
.onchain_report,
|
||||||
.lightning_report,
|
.lightning_report,
|
||||||
|
.lightning_error,
|
||||||
=> last_report.replace(msg),
|
=> 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 => {
|
else => {
|
||||||
logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)});
|
logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)});
|
||||||
msg.deinit();
|
msg.deinit();
|
||||||
|
@ -256,10 +268,16 @@ fn commThreadLoopCycle() !void {
|
||||||
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
|
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
|
||||||
last_report.replace(msg);
|
last_report.replace(msg);
|
||||||
},
|
},
|
||||||
.lightning_report => |rep| {
|
.lightning_report, .lightning_error => {
|
||||||
ui.lightning.updateTabPanel(rep) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
|
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
|
||||||
last_report.replace(msg);
|
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| {
|
.settings => |sett| {
|
||||||
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
|
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
|
||||||
msg.deinit();
|
msg.deinit();
|
||||||
|
@ -311,6 +329,11 @@ fn uiThreadLoop() void {
|
||||||
logger.err("bitcoin.updateTabPanel: {any}", .{err});
|
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;
|
continue;
|
||||||
},
|
},
|
||||||
|
@ -383,7 +406,7 @@ pub fn main() anyerror!void {
|
||||||
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
|
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
|
||||||
|
|
||||||
// initalizes display, input driver and finally creates the user interface.
|
// 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});
|
logger.err("ui.init: {any}", .{err});
|
||||||
return 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});
|
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 => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,6 +150,8 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
|
||||||
var block_count: u32 = 801365;
|
var block_count: u32 = 801365;
|
||||||
var settings_sent = false;
|
var settings_sent = false;
|
||||||
|
|
||||||
|
var lnd_uninited_sent = false;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
time.sleep(time.ns_per_s);
|
time.sleep(time.ns_per_s);
|
||||||
if (sectimer.read() < 3 * 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});
|
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) {
|
if (block_count % 2 == 0) {
|
||||||
const lndrep: comm.Message.LightningReport = .{
|
const lndrep: comm.Message.LightningReport = .{
|
||||||
.version = "0.16.4-beta commit=v0.16.4-beta",
|
.version = "0.16.4-beta commit=v0.16.4-beta",
|
||||||
|
|
|
@ -10,11 +10,22 @@ pub fn main() !void {
|
||||||
|
|
||||||
var client = try lndhttp.Client.init(.{
|
var client = try lndhttp.Client.init(.{
|
||||||
.allocator = gpa,
|
.allocator = gpa,
|
||||||
|
.port = 10010,
|
||||||
.tlscert_path = "/home/lnd/.lnd/tls.cert",
|
.tlscert_path = "/home/lnd/.lnd/tls.cert",
|
||||||
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
|
.macaroon_ro_path = "/ssd/lnd/data/chain/bitcoin/mainnet/readonly.macaroon",
|
||||||
});
|
});
|
||||||
defer client.deinit();
|
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, {});
|
const res = try client.call(.getinfo, {});
|
||||||
defer res.deinit();
|
defer res.deinit();
|
||||||
|
@ -31,13 +42,13 @@ pub fn main() !void {
|
||||||
// std.debug.print("{any}\n", .{res.value.channels});
|
// 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, {});
|
// const res = try client.call(.feereport, {});
|
||||||
// defer res.deinit();
|
// defer res.deinit();
|
||||||
// std.debug.print("{any}\n", .{res.value});
|
// 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 {
|
pub const StringList = struct {
|
||||||
l: std.ArrayList([]const u8),
|
l: std.ArrayList([]const u8),
|
||||||
allocator: std.mem.Allocator,
|
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| {
|
for (self.l.items) |a| {
|
||||||
self.allocator.free(a);
|
self.allocator.free(a);
|
||||||
}
|
}
|
||||||
|
|
|
@ -605,7 +605,7 @@
|
||||||
#define LV_USE_GIF 0
|
#define LV_USE_GIF 0
|
||||||
|
|
||||||
/*QR code library*/
|
/*QR code library*/
|
||||||
#define LV_USE_QRCODE 0
|
#define LV_USE_QRCODE 1
|
||||||
|
|
||||||
/*FreeType library*/
|
/*FreeType library*/
|
||||||
#define LV_USE_FREETYPE 0
|
#define LV_USE_FREETYPE 0
|
||||||
|
|
|
@ -92,6 +92,16 @@ extern lv_style_t *nm_style_title()
|
||||||
return &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)
|
static void textarea_event_cb(lv_event_t *e)
|
||||||
{
|
{
|
||||||
lv_obj_t *textarea = lv_event_get_target(e);
|
lv_obj_t *textarea = lv_event_get_target(e);
|
||||||
|
|
|
@ -5,15 +5,52 @@ const std = @import("std");
|
||||||
|
|
||||||
const comm = @import("../comm.zig");
|
const comm = @import("../comm.zig");
|
||||||
const lvgl = @import("lvgl.zig");
|
const lvgl = @import("lvgl.zig");
|
||||||
|
const symbol = @import("symbol.zig");
|
||||||
|
const types = @import("../types.zig");
|
||||||
const xfmt = @import("../xfmt.zig");
|
const xfmt = @import("../xfmt.zig");
|
||||||
|
const widget = @import("widget.zig");
|
||||||
|
|
||||||
const logger = std.log.scoped(.ui_lnd);
|
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"
|
/// label color mark start to make "label:" part of a "label: value"
|
||||||
/// in a different color.
|
/// in a different color.
|
||||||
const cmark = "#bbbbbb ";
|
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 {
|
var tab: struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
info: struct {
|
info: struct {
|
||||||
|
card: lvgl.Card, // parent
|
||||||
alias: lvgl.Label,
|
alias: lvgl.Label,
|
||||||
blockhash: lvgl.Label,
|
blockhash: lvgl.Label,
|
||||||
currblock: lvgl.Label,
|
currblock: lvgl.Label,
|
||||||
|
@ -22,6 +59,7 @@ var tab: struct {
|
||||||
version: lvgl.Label,
|
version: lvgl.Label,
|
||||||
},
|
},
|
||||||
balance: struct {
|
balance: struct {
|
||||||
|
card: lvgl.Card, // parent
|
||||||
avail: lvgl.Bar, // local vs remote
|
avail: lvgl.Bar, // local vs remote
|
||||||
local: lvgl.Label,
|
local: lvgl.Label,
|
||||||
remote: lvgl.Label,
|
remote: lvgl.Label,
|
||||||
|
@ -29,18 +67,119 @@ var tab: struct {
|
||||||
pending: lvgl.Label,
|
pending: lvgl.Label,
|
||||||
fees: lvgl.Label, // day, week, month
|
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;
|
} = undefined;
|
||||||
|
|
||||||
/// creates the tab content with all elements.
|
/// creates the tab content with all elements.
|
||||||
/// must be called only once at UI init.
|
/// 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 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
|
// info section
|
||||||
{
|
{
|
||||||
const card = try lvgl.Card.new(parent, "INFO", .{});
|
tab.info.card = try lvgl.Card.new(parent, "INFO", .{});
|
||||||
const row = try lvgl.FlexLayout.new(card, .row, .{});
|
const row = try lvgl.FlexLayout.new(tab.info.card, .row, .{});
|
||||||
row.setHeightToContent();
|
row.setHeightToContent();
|
||||||
row.setWidth(lvgl.sizePercent(100));
|
row.setWidth(lvgl.sizePercent(100));
|
||||||
row.clearFlag(.scrollable);
|
row.clearFlag(.scrollable);
|
||||||
|
@ -49,22 +188,22 @@ pub fn initTabPanel(cont: lvgl.Container) !void {
|
||||||
left.setHeightToContent();
|
left.setHeightToContent();
|
||||||
left.setWidth(lvgl.sizePercent(50));
|
left.setWidth(lvgl.sizePercent(50));
|
||||||
left.setPad(10, .row, .{});
|
left.setPad(10, .row, .{});
|
||||||
tab.info.alias = try lvgl.Label.new(left, "ALIAS\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 = true });
|
tab.info.pubkey = try lvgl.Label.new(left, "PUBKEY\n", recolor);
|
||||||
tab.info.version = try lvgl.Label.new(left, "VERSION\n", .{ .recolor = true });
|
tab.info.version = try lvgl.Label.new(left, "VERSION\n", recolor);
|
||||||
// right column
|
// right column
|
||||||
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
right.setHeightToContent();
|
right.setHeightToContent();
|
||||||
right.setWidth(lvgl.sizePercent(50));
|
right.setWidth(lvgl.sizePercent(50));
|
||||||
right.setPad(10, .row, .{});
|
right.setPad(10, .row, .{});
|
||||||
tab.info.currblock = try lvgl.Label.new(right, "HEIGHT\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 = true });
|
tab.info.blockhash = try lvgl.Label.new(right, "BLOCK HASH\n", recolor);
|
||||||
tab.info.npeers = try lvgl.Label.new(right, "CONNECTED PEERS\n", .{ .recolor = true });
|
tab.info.npeers = try lvgl.Label.new(right, "CONNECTED PEERS\n", recolor);
|
||||||
}
|
}
|
||||||
// balance section
|
// balance section
|
||||||
{
|
{
|
||||||
const card = try lvgl.Card.new(parent, "BALANCE", .{});
|
tab.balance.card = try lvgl.Card.new(parent, "BALANCE", .{});
|
||||||
const row = try lvgl.FlexLayout.new(card, .row, .{});
|
const row = try lvgl.FlexLayout.new(tab.balance.card, .row, .{});
|
||||||
row.setWidth(lvgl.sizePercent(100));
|
row.setWidth(lvgl.sizePercent(100));
|
||||||
row.clearFlag(.scrollable);
|
row.clearFlag(.scrollable);
|
||||||
// left column
|
// left column
|
||||||
|
@ -76,31 +215,298 @@ pub fn initTabPanel(cont: lvgl.Container) !void {
|
||||||
const subrow = try lvgl.FlexLayout.new(left, .row, .{ .main = .space_between });
|
const subrow = try lvgl.FlexLayout.new(left, .row, .{ .main = .space_between });
|
||||||
subrow.setWidth(lvgl.sizePercent(90));
|
subrow.setWidth(lvgl.sizePercent(90));
|
||||||
subrow.setHeightToContent();
|
subrow.setHeightToContent();
|
||||||
tab.balance.local = try lvgl.Label.new(subrow, "LOCAL\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 = true });
|
tab.balance.remote = try lvgl.Label.new(subrow, "REMOTE\n", recolor);
|
||||||
// right column
|
// right column
|
||||||
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
const right = try lvgl.FlexLayout.new(row, .column, .{});
|
||||||
right.setWidth(lvgl.sizePercent(50));
|
right.setWidth(lvgl.sizePercent(50));
|
||||||
right.setPad(10, .row, .{});
|
right.setPad(10, .row, .{});
|
||||||
tab.balance.pending = try lvgl.Label.new(right, "PENDING\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 = true });
|
tab.balance.unsettled = try lvgl.Label.new(right, "UNSETTLED\n", recolor);
|
||||||
// bottom
|
// 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
|
// channels section
|
||||||
{
|
{
|
||||||
const card = try lvgl.Card.new(parent, "CHANNELS", .{});
|
tab.channels.card = try lvgl.Card.new(parent, "CHANNELS", .{});
|
||||||
tab.channels_cont = try lvgl.FlexLayout.new(card, .column, .{});
|
tab.channels.cont = try lvgl.FlexLayout.new(tab.channels.card, .column, .{});
|
||||||
tab.channels_cont.setHeightToContent();
|
tab.channels.cont.setHeightToContent();
|
||||||
tab.channels_cont.setWidth(lvgl.sizePercent(100));
|
tab.channels.cont.setWidth(lvgl.sizePercent(100));
|
||||||
tab.channels_cont.clearFlag(.scrollable);
|
tab.channels.cont.clearFlag(.scrollable);
|
||||||
tab.channels_cont.setPad(10, .row, .{});
|
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.
|
fn promptNodeReset() !void {
|
||||||
/// the tab must be inited first with initTabPanel.
|
const proceed: [*:0]const u8 = "PROCEED"; // btn idx 0
|
||||||
pub fn updateTabPanel(rep: comm.Message.LightningReport) !void {
|
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;
|
var buf: [512]u8 = undefined;
|
||||||
|
|
||||||
// info section
|
// info section
|
||||||
|
@ -132,9 +538,9 @@ pub fn updateTabPanel(rep: comm.Message.LightningReport) !void {
|
||||||
});
|
});
|
||||||
|
|
||||||
// channels section
|
// channels section
|
||||||
tab.channels_cont.deleteChildren();
|
tab.channels.cont.deleteChildren();
|
||||||
for (rep.channels) |ch| {
|
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.setWidth(lvgl.sizePercent(100));
|
||||||
chbox.setHeightToContent();
|
chbox.setHeightToContent();
|
||||||
_ = try switch (ch.state) {
|
_ = try switch (ch.state) {
|
||||||
|
|
|
@ -162,6 +162,10 @@ pub const LvEvent = opaque {
|
||||||
pub fn userdata(self: *LvEvent) ?*anyopaque {
|
pub fn userdata(self: *LvEvent) ?*anyopaque {
|
||||||
return lv_event_get_user_data(self);
|
return lv_event_get_user_data(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stopBubbling(self: *LvEvent) void {
|
||||||
|
lv_event_stop_bubbling(self);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// represents lv_disp_t in C.
|
/// represents lv_disp_t in C.
|
||||||
|
@ -350,6 +354,12 @@ pub const BaseObjMethods = struct {
|
||||||
nm_obj_set_userdata(self.lvobj, data);
|
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.
|
/// 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.
|
/// to make cb called on any event, use EventCode.all filter.
|
||||||
/// multiple event handlers are called in the same order as they were added.
|
/// 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.
|
/// methods applicable to visible objects like labels, buttons and containers.
|
||||||
pub const WidgetMethods = struct {
|
pub const WidgetMethods = struct {
|
||||||
|
pub fn contentWidth(self: anytype) Coord {
|
||||||
|
return lv_obj_get_content_width(self.lvobj);
|
||||||
|
}
|
||||||
|
|
||||||
/// sets object horizontal length.
|
/// sets object horizontal length.
|
||||||
pub fn setWidth(self: anytype, val: Coord) void {
|
pub fn setWidth(self: anytype, val: Coord) void {
|
||||||
lv_obj_set_width(self.lvobj, val);
|
lv_obj_set_width(self.lvobj, val);
|
||||||
|
@ -553,6 +567,8 @@ pub const FlexLayout = struct {
|
||||||
cross: AlignCross = .start,
|
cross: AlignCross = .start,
|
||||||
track: Align = .start,
|
track: Align = .start,
|
||||||
all: ?Align = null, // overrides all 3 above
|
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.
|
/// 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());
|
lv_obj_remove_style(obj, null, bgsel.value());
|
||||||
const flex = adopt(obj, flow, opt);
|
const flex = adopt(obj, flow, opt);
|
||||||
flex.padColumnDefault();
|
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;
|
return flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -849,7 +874,7 @@ pub const Dropdown = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns selected option as a slice of the buf.
|
/// 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 {
|
pub fn getSelectedStr(self: Dropdown, buf: []u8) [:0]const u8 {
|
||||||
const buflen: u32 = @min(buf.len, std.math.maxInt(u32));
|
const buflen: u32 = @min(buf.len, std.math.maxInt(u32));
|
||||||
lv_dropdown_get_selected_str(self.lvobj, buf.ptr, buflen);
|
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.
|
/// represents lv_obj_t type in C.
|
||||||
pub const LvObj = opaque {
|
pub const LvObj = opaque {
|
||||||
/// feature-flags controlling object's behavior.
|
/// 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_current_target(e: *LvEvent) *LvObj;
|
||||||
extern fn lv_event_get_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_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;
|
extern fn lv_obj_add_event_cb(obj: *LvObj, cb: LvEvent.Callback, filter: LvEvent.Code, userdata: ?*anyopaque) *LvEvent.Descriptor;
|
||||||
|
|
||||||
// display and screen functions ----------------------------------------------
|
// display and screen functions ----------------------------------------------
|
||||||
|
@ -1085,6 +1139,8 @@ extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj;
|
||||||
extern fn lv_obj_del(obj: *LvObj) void;
|
extern fn lv_obj_del(obj: *LvObj) void;
|
||||||
/// deletes children of the obj.
|
/// deletes children of the obj.
|
||||||
extern fn lv_obj_clean(obj: *LvObj) void;
|
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_add_state(obj: *LvObj, c.lv_state_t) void;
|
||||||
extern fn lv_obj_clear_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_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_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_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_flow(obj: *LvObj, flow: c.lv_flex_flow_t) void;
|
||||||
extern fn lv_obj_set_flex_grow(obj: *LvObj, val: u8) 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_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_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj;
|
||||||
extern fn lv_win_get_content(win: *LvObj) *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
|
///! see lv_symbols_def.h
|
||||||
|
pub const LightningBolt = &[_]u8{ 0xef, 0x83, 0xa7 };
|
||||||
pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 };
|
pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 };
|
||||||
pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c };
|
pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c };
|
||||||
pub const Power = &[_]u8{ 0xef, 0x80, 0x91 };
|
pub const Power = &[_]u8{ 0xef, 0x80, 0x91 };
|
||||||
|
pub const Right = &[_]u8{ 0xef, 0x81, 0x94 };
|
||||||
pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 };
|
pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 };
|
||||||
|
|
|
@ -14,9 +14,16 @@ pub const settings = @import("settings.zig");
|
||||||
|
|
||||||
const logger = std.log.scoped(.ui);
|
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;
|
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();
|
lvgl.init();
|
||||||
const disp = try drv.initDisplay();
|
const disp = try drv.initDisplay();
|
||||||
drv.initInput() catch |err| {
|
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 {
|
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});
|
logger.err("createLightningPanel: {any}", .{err});
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
Reference in New Issue