From 979bb30c15c13d521c3ffafcd4c69771ca211279 Mon Sep 17 00:00:00 2001 From: Nikita Orlov Date: Thu, 12 Sep 2024 12:03:54 +0200 Subject: [PATCH] toml configuration + run from configuration (#18) --- build.zig | 8 ++ build.zig.zon | 8 ++ src/core/database/mint_memory.zig | 44 ++++++ src/core/mint/mint.zig | 4 +- src/fake_wallet/fake_wallet.zig | 8 +- src/mint.zig | 226 +++++++++++++++++++++++------- src/mintd/config.example.toml | 39 ++++++ src/mintd/config.zig | 94 +++++++++++++ 8 files changed, 378 insertions(+), 53 deletions(-) create mode 100644 src/mintd/config.example.toml create mode 100644 src/mintd/config.zig diff --git a/build.zig b/build.zig index 6123fde..16bcc1c 100644 --- a/build.zig +++ b/build.zig @@ -21,6 +21,14 @@ const external_dependencies = [_]build_helpers.Dependency{ .name = "zig-cli", .module_name = "zig-cli", }, + .{ + .name = "zig-toml", + .module_name = "zig-toml", + }, + .{ + .name = "clap", + .module_name = "clap", + }, }; pub fn build(b: *std.Build) !void { diff --git a/build.zig.zon b/build.zig.zon index 2e2b263..25647c6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -43,6 +43,14 @@ .url = "git+https://github.com/zig-bitcoin/bitcoin-primitives#b51ffa5b67e376a102bde0250e4235bc66e32c2e", .hash = "1220ae99270542861a0f2cc5d9b0b2df2f11c6bd2ce431c9067c5c958cd36f66c948", }, + .@"zig-toml" = .{ + .url = "git+https://github.com/sam701/zig-toml#78c8512273ab83c0a71f1063d9049ce7abdb70b0", + .hash = "122090781267644f32a5ab9c0be0eae71df5ac3401f017e39dd6a58f583b7e10f1d1", + }, + .clap = .{ + .url = "git+https://github.com/Hejsil/zig-clap#2d9db156ae928860a9acf2f1260750d3b44a4c98", + .hash = "122005e589ab3b6bff8e589b45f5b12cd27ce79f266bdac17e9f33ebfe2fbaff7fe3", + }, }, .paths = .{ "build.zig", diff --git a/src/core/database/mint_memory.zig b/src/core/database/mint_memory.zig index 9f7de5c..8cde912 100644 --- a/src/core/database/mint_memory.zig +++ b/src/core/database/mint_memory.zig @@ -27,6 +27,50 @@ pub const MintMemoryDatabase = struct { allocator: std.mem.Allocator, + pub fn deinit(self: *MintMemoryDatabase) void { + self.active_keysets.deinit(); + + { + var it = self.keysets.iterator(); + while (it.next()) |entry| { + entry.value_ptr.*.deinit(self.allocator); + } + + self.keysets.deinit(); + } + + { + var it = self.mint_quotes.iterator(); + while (it.next()) |entry| { + entry.value_ptr.*.deinit(self.allocator); + } + + self.mint_quotes.deinit(); + } + + { + var it = self.melt_quotes.iterator(); + while (it.next()) |entry| { + entry.value_ptr.*.deinit(self.allocator); + } + + self.melt_quotes.deinit(); + } + + { + var it = self.proofs.iterator(); + while (it.next()) |entry| { + entry.value_ptr.*.deinit(self.allocator); + } + + self.proofs.deinit(); + } + + self.proof_states.deinit(); + + self.blinded_signatures.deinit(); + } + /// initFrom - take own on all data there, except slices (only own data in slices) pub fn initFrom( allocator: std.mem.Allocator, diff --git a/src/core/mint/mint.zig b/src/core/mint/mint.zig index ebb2cec..24aae38 100644 --- a/src/core/mint/mint.zig +++ b/src/core/mint/mint.zig @@ -23,9 +23,9 @@ pub const MintMemoryDatabase = core.mint_memory.MintMemoryDatabase; /// Mint Fee Reserve pub const FeeReserve = struct { /// Absolute expected min fee - min_fee_reserve: core.amount.Amount, + min_fee_reserve: core.amount.Amount = 0, /// Percentage expected fee - percent_fee_reserve: f32, + percent_fee_reserve: f32 = 0, }; /// Mint Keyset Info diff --git a/src/fake_wallet/fake_wallet.zig b/src/fake_wallet/fake_wallet.zig index 06ed9f1..014eacb 100644 --- a/src/fake_wallet/fake_wallet.zig +++ b/src/fake_wallet/fake_wallet.zig @@ -26,10 +26,10 @@ const MintQuoteState = core.nuts.nut04.QuoteState; pub const FakeWallet = struct { const Self = @This(); - fee_reserve: core.mint.FeeReserve, - chan: *Channel([]const u8), // we using signle channel for sending invoices - mint_settings: MintMeltSettings, - melt_settings: MintMeltSettings, + fee_reserve: core.mint.FeeReserve = .{}, + chan: *Channel([]const u8) = undefined, // we using signle channel for sending invoices + mint_settings: MintMeltSettings = .{}, + melt_settings: MintMeltSettings = .{}, /// Creat init [`FakeWallet`] pub fn init( diff --git a/src/mint.zig b/src/mint.zig index d6545b0..55c8e51 100644 --- a/src/mint.zig +++ b/src/mint.zig @@ -6,24 +6,22 @@ const bip39 = bitcoin_primitives.bips.bip39; const core = @import("core/lib.zig"); const os = std.os; const builtin = @import("builtin"); +const config = @import("mintd/config.zig"); +const clap = @import("clap"); const MintState = @import("router/router.zig").MintState; const LnKey = @import("router/router.zig").LnKey; const FakeWallet = @import("fake_wallet/fake_wallet.zig").FakeWallet; const Mint = core.mint.Mint; +const FeeReserve = core.mint.FeeReserve; const MintDatabase = core.mint_memory.MintMemoryDatabase; +const ContactInfo = core.nuts.ContactInfo; +const MintVersion = core.nuts.MintVersion; +const MintInfo = core.nuts.MintInfo; -/// The default log level is based on build mode. -pub const default_level: std.log.Level = switch (builtin.mode) { - .Debug => .debug, - .ReleaseSafe => .notice, - .ReleaseFast => .err, - .ReleaseSmall => .err, -}; +const default_quote_ttl_secs: u64 = 1800; pub fn main() !void { - const settings_info_mnemonic = "few oppose awkward uncover next patrol goose spike depth zebra brick cactus"; - var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 20, }).init; @@ -32,7 +30,96 @@ pub fn main() !void { std.debug.assert(gpa.deinit() == .ok); } - const mnemonic = try bip39.Mnemonic.parseInNormalized(.english, settings_info_mnemonic); + // parsing CLI + + var clap_res = v: { + // First we specify what parameters our program can take. + // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)` + const params = comptime clap.parseParamsComptime( + \\-h, --help Display this help and exit. + \\-c, --config Use the as the location of the config file. + \\ + ); + + // Initialize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also pass `.{}` to `clap.parse` if you don't + // care about the extra information `Diagnostics` provides. + var diag = clap.Diagnostic{}; + var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ + .diagnostic = &diag, + .allocator = gpa.allocator(), + }) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().writer(), err) catch {}; + return err; + }; + errdefer res.deinit(); + + // helper to print help + if (res.args.help != 0) + return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); + + break :v res; + }; + defer clap_res.deinit(); + + const config_path = clap_res.args.config orelse "config.toml"; + + var parsed_settings = try config.Settings.initFromToml(gpa.allocator(), config_path); + defer parsed_settings.deinit(); + + var localstore = switch (parsed_settings.value.database.engine) { + .in_memory => v: { + break :v try MintDatabase.initFrom( + gpa.allocator(), + .init(gpa.allocator()), + &.{}, + &.{}, + &.{}, + &.{}, + &.{}, + .init(gpa.allocator()), + ); + }, + else => { + // not implemented engine + unreachable; + }, + }; + defer localstore.deinit(); + + var contact_info = std.ArrayList(ContactInfo).init(gpa.allocator()); + defer contact_info.deinit(); + + if (parsed_settings.value.mint_info.contact_nostr_public_key) |nostr_contact| { + try contact_info.append(.{ + .method = "nostr", + .info = nostr_contact, + }); + } + + if (parsed_settings.value.mint_info.contact_email) |email_contact| { + try contact_info.append(.{ + .method = "email", + .info = email_contact, + }); + } + + const mint_version = MintVersion{ + .name = "mint-server", + .version = "1.0.0", // TODO version + }; + + const relative_ln_fee = parsed_settings.value.ln.fee_percent; + + const absolute_ln_fee_reserve = parsed_settings.value.ln.reserve_fee_min; + + const fee_reserve = FeeReserve{ + .min_fee_reserve = absolute_ln_fee_reserve, + .percent_fee_reserve = relative_ln_fee, + }; + + const input_fee_ppk = parsed_settings.value.info.input_fee_ppk orelse 0; var supported_units = std.AutoHashMap(core.nuts.CurrencyUnit, std.meta.Tuple(&.{ u64, u8 })).init(gpa.allocator()); defer supported_units.deinit(); @@ -46,58 +133,103 @@ pub fn main() !void { ln_backends.deinit(); } - // TODO ln_routers? - // init ln backend - { - const units: []const core.nuts.CurrencyUnit = &.{.sat}; + // TODO set ln router + // additional routers for httpz server + switch (parsed_settings.value.ln.ln_backend) { + .fake_wallet => { + const units = (parsed_settings.value.fake_wallet orelse config.FakeWallet{}).supported_units; - for (units) |unit| { - const ln_key = LnKey.init(unit, .bolt11); + for (units) |unit| { + const ln_key = LnKey.init(unit, .bolt11); - const wallet = try FakeWallet.init( - gpa.allocator(), - .{ - .min_fee_reserve = 1, - .percent_fee_reserve = 1.0, - }, - .{}, - .{}, - ); + var wallet = try FakeWallet.init(gpa.allocator(), fee_reserve, .{}, .{}); + errdefer wallet.deinit(); - try ln_backends.put(ln_key, wallet); + try ln_backends.put(ln_key, wallet); - try supported_units.put(unit, .{ 0, 64 }); - } + try supported_units.put(unit, .{ input_fee_ppk, 64 }); + } + }, + else => { + // not implemented backends + unreachable; + }, } - var db = try MintDatabase.initManaged(gpa.allocator()); - defer db.deinit(); - - var mint = try Mint.init(gpa.allocator(), "MintUrl", &try mnemonic.toSeedNormalized(&.{}), .{ - .name = "dfdf", - .pubkey = null, - .version = null, - .description = "dfdf", - .description_long = null, - .contact = null, - .nuts = .{}, - .mint_icon_url = null, - .motd = null, - }, &db.value, supported_units); + // TODO nuts settings + + const mint_info = MintInfo{ + .name = parsed_settings.value.mint_info.name, + .version = mint_version, + .description = parsed_settings.value.mint_info.description, + .description_long = parsed_settings.value.mint_info.description_long, + .contact = contact_info.items, + .pubkey = parsed_settings.value.mint_info.pubkey, + .mint_icon_url = parsed_settings.value.mint_info.mint_icon_url, + .motd = parsed_settings.value.mint_info.motd, + }; + + const mnemonic = try bip39.Mnemonic.parseInNormalized(.english, parsed_settings.value.info.mnemonic); + + var mint = try Mint.init(gpa.allocator(), parsed_settings.value.info.url, &try mnemonic.toSeedNormalized(&.{}), mint_info, &localstore, supported_units); defer mint.deinit(); - var srv = try router.createMintServer(gpa.allocator(), "MintUrl", &mint, ln_backends, 15, .{ - .port = 5500, - .address = "0.0.0.0", + // Check the status of any mint quotes that are pending + // In the event that the mint server is down but the ln node is not + // it is possible that a mint quote was paid but the mint has not been updated + // this will check and update the mint state of those quotes + // for ln in ln_backends.values() { + // check_pending_quotes(Arc::clone(&mint), Arc::clone(ln)).await?; + // } + // TODO + + const mint_url = parsed_settings.value.info.url; + const listen_addr = parsed_settings.value.info.listen_host; + const listen_port = parsed_settings.value.info.listen_port; + const quote_ttl = parsed_settings.value + .info + .seconds_quote_is_valid_for orelse default_quote_ttl_secs; + + // start serevr + var srv = try router.createMintServer(gpa.allocator(), mint_url, &mint, ln_backends, quote_ttl, .{ + .port = listen_port, + .address = listen_addr, }); defer srv.deinit(); + // add lnn router here to server try handleInterrupt(&srv); - std.log.info("Listening server", .{}); + + // Spawn task to wait for invoces to be paid and update mint quotes + // handle invoices + // for (_, ln) in ln_backends { + // let mint = Arc::clone(&mint); + // tokio::spawn(async move { + // loop { + // match ln.wait_any_invoice().await { + // Ok(mut stream) => { + // while let Some(request_lookup_id) = stream.next().await { + // if let Err(err) = + // handle_paid_invoice(mint.clone(), &request_lookup_id).await + // { + // tracing::warn!("{:?}", err); + // } + // } + // } + // Err(err) => { + // tracing::warn!("Could not get invoice stream: {}", err); + // } + // } + // } + // }); + // } + + std.log.info("Listening server on {s}:{d}", .{ + parsed_settings.value.info.listen_host, parsed_settings.value.info.listen_port, + }); try srv.listen(); std.log.info("Stopped server", .{}); - // router.createMintServer(gpa.allocator(), bip39., , , ) } pub fn handleInterrupt(srv: *httpz.Server(MintState)) !void { diff --git a/src/mintd/config.example.toml b/src/mintd/config.example.toml new file mode 100644 index 0000000..e68a07f --- /dev/null +++ b/src/mintd/config.example.toml @@ -0,0 +1,39 @@ + +[info] +url = "https://mint.thesimplekid.dev/" +listen_host = "127.0.0.1" +listen_port = 8085 +mnemonic = "" +# input_fee_ppk = 0 + + + +[mint_info] +# name = "cdk-mintd mutiney net mint" +# Hex publey of mint +# pubkey = "" +# description = "These are not real sats for testing only" +# description_long = "A longer mint for testing" +# motd = "Hello world" +# mint_icon_url = "https://this-is-a-mint-icon-url.com/icon.png" +# contact_email = "hello@cashu.me" +# Nostr pubkey of mint (Hex) +# contact_nostr_public_key = "" + + +[database] +# Database engine (sqlite/redb) defaults to sqlite +# engine = "sqlite" + +[ln] +# Required ln backend `cln`, `strike`, `fakewallet` +ln_backend = "cln" + +# [cln] +# Required if using cln backend path to rpc +# cln_path = "" + +# [strike] +# api_key="" +# Optional default sats +# supported_units=[""] diff --git a/src/mintd/config.zig b/src/mintd/config.zig new file mode 100644 index 0000000..eb89ab4 --- /dev/null +++ b/src/mintd/config.zig @@ -0,0 +1,94 @@ +const std = @import("std"); +const core = @import("../core/lib.zig"); +const bitcoin_primitives = @import("bitcoin-primitives"); +const zig_toml = @import("zig-toml"); + +const PublicKey = bitcoin_primitives.secp256k1.PublicKey; +const Amount = core.amount.Amount; +const CurrencyUnit = core.nuts.CurrencyUnit; + +pub const Settings = struct { + info: Info, + mint_info: MintInfo, + ln: Ln, + cln: ?Cln, + strike: ?Strike, + fake_wallet: ?FakeWallet, + database: Database, + + pub fn initFromToml(gpa: std.mem.Allocator, config_file_name: []const u8) !zig_toml.Parsed(Settings) { + var parser = zig_toml.Parser(Settings).init(gpa); + defer parser.deinit(); + + const result = try parser.parseFile(config_file_name); + + return result; + } +}; + +pub const DatabaseEngine = enum { + sqlite, + redb, + in_memory, +}; + +pub const Database = struct { + engine: DatabaseEngine = .in_memory, +}; + +pub const Info = struct { + url: []const u8, + listen_host: []const u8, + listen_port: u16, + mnemonic: []const u8, + seconds_quote_is_valid_for: ?u64, + input_fee_ppk: ?u64, +}; + +pub const MintInfo = struct { + /// name of the mint and should be recognizable + name: []const u8, + /// hex pubkey of the mint + pubkey: ?PublicKey, // nut01 + /// short description of the mint + description: []const u8, + /// long description + description_long: ?[]const u8, + /// url to the mint icon + mint_icon_url: ?[]const u8, + /// message of the day that the wallet must display to the user + motd: ?[]const u8, + /// Nostr publickey + contact_nostr_public_key: ?[]const u8, + /// Contact email + contact_email: ?[]const u8, +}; + +pub const LnBackend = enum { + // default + cln, + strike, + fake_wallet, + // Greenlight, + // Ldk, +}; + +pub const Ln = struct { + ln_backend: LnBackend = .cln, + invoice_description: ?[]const u8, + fee_percent: f32, + reserve_fee_min: Amount, +}; + +pub const Strike = struct { + api_key: []const u8, + supported_units: ?[]const CurrencyUnit, +}; + +pub const Cln = struct { + rpc_path: []const u8, +}; + +pub const FakeWallet = struct { + supported_units: []const CurrencyUnit = &.{.sat}, +};