From 1074273e78c11d1c8895bc651a6d1168ccd20a4c Mon Sep 17 00:00:00 2001 From: Nikita Orlov Date: Fri, 6 Sep 2024 17:17:02 +0200 Subject: [PATCH] Fake lightning wallet + mint core (#10) * fake lightning wallet * nut04 mint resp * some mint methods + types --- build.zig | 9 + src/channels/channels.zig | 335 +++++++++++++++++++++++++++++++ src/core/lightning/lightning.zig | 12 ++ src/core/mint/mint.zig | 79 +++++++- src/core/mint/types.zig | 23 ++- src/core/nuts/nut04/nut04.zig | 131 ++++++++++++ src/fake_wallet/fake_wallet.zig | 127 +++++++++++- src/fake_wallet/pub_sub.zig | 1 + src/helper/helper.zig | 15 ++ 9 files changed, 717 insertions(+), 15 deletions(-) create mode 100644 src/channels/channels.zig create mode 100644 src/fake_wallet/pub_sub.zig diff --git a/build.zig b/build.zig index d4e9df3..b3990aa 100644 --- a/build.zig +++ b/build.zig @@ -23,6 +23,13 @@ pub fn build(b: *std.Build) !void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); + // Channel module + const channel_m = b.addModule("channels", .{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/channels/channels.zig"), + }); + // ************************************************************** // * HANDLE DEPENDENCY MODULES * // ************************************************************** @@ -199,6 +206,8 @@ pub fn build(b: *std.Build) !void { lib_unit_tests.root_module.addImport("bitcoin", bitcoin_zig.module("bitcoin")); lib_unit_tests.root_module.addImport("base58", base58_module); + lib_unit_tests.root_module.addImport("channels", channel_m); + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); const test_step = b.step("test", "Run unit tests"); diff --git a/src/channels/channels.zig b/src/channels/channels.zig new file mode 100644 index 0000000..2fb672b --- /dev/null +++ b/src/channels/channels.zig @@ -0,0 +1,335 @@ +const std = @import("std"); + +const ChanError = error{ + Closed, + OutOfMemory, + NotImplemented, + DataCorruption, +}; + +pub fn Chan(comptime T: type) type { + return BufferedChan(T, 0); +} + +pub fn BufferedChan(comptime T: type, comptime bufSize: u8) type { + return struct { + const Self = @This(); + const bufType = [bufSize]?T; + buf: bufType = [_]?T{null} ** bufSize, + closed: bool = false, + mut: std.Thread.Mutex = std.Thread.Mutex{}, + alloc: std.mem.Allocator = undefined, + recvQ: std.ArrayList(*Receiver) = undefined, + sendQ: std.ArrayList(*Sender) = undefined, + + // represents a thread waiting on recv + const Receiver = struct { + mut: std.Thread.Mutex = std.Thread.Mutex{}, + cond: std.Thread.Condition = std.Thread.Condition{}, + data: ?T = null, + + fn putDataAndSignal(self: *@This(), data: T) void { + defer self.cond.signal(); + self.data = data; + } + }; + + // represents a thread waiting on send + const Sender = struct { + mut: std.Thread.Mutex = std.Thread.Mutex{}, + cond: std.Thread.Condition = std.Thread.Condition{}, + data: T, + + fn getDataAndSignal(self: *@This()) T { + defer self.cond.signal(); + return self.data; + } + }; + + pub fn init(alloc: std.mem.Allocator) Self { + return Self{ + .alloc = alloc, + .recvQ = std.ArrayList(*Receiver).init(alloc), + .sendQ = std.ArrayList(*Sender).init(alloc), + }; + } + + pub fn deinit(self: *Self) void { + self.recvQ.deinit(); + self.sendQ.deinit(); + } + + pub fn close(self: *Self) void { + self.closed = true; + } + + pub fn capacity(self: *Self) u8 { + return self.buf.len; + } + + pub fn debugBuf(self: *Self) void { + std.debug.print("{d} Buffer debug\n", .{std.time.milliTimestamp()}); + for (self.buf, 0..) |item, i| { + if (item) |unwrapped| { + std.debug.print("[{d}] = {d}\n", .{ i, unwrapped }); + } + } + } + + pub fn len(self: *Self) u8 { + var i: u8 = 0; + for (self.buf) |item| { + if (item) |_| { + i += 1; + } else { + break; + } + } + return i; + } + + pub fn send(self: *Self, data: T) ChanError!void { + if (self.closed) return ChanError.Closed; + + self.mut.lock(); + errdefer self.mut.unlock(); + + // case: receiver already waiting + // pull receiver (if any) and give it data. Signal receiver that it's done waiting. + if (self.recvQ.items.len > 0) { + defer self.mut.unlock(); + var receiver: *Receiver = self.recvQ.orderedRemove(0); + receiver.putDataAndSignal(data); + return; + } + + // case: room in buffer + const l = self.len(); + if (l < self.capacity() and bufSize > 0) { + defer self.mut.unlock(); + + // insert into first null spot in buffer + self.buf[l] = data; + return; + } + + // hold on sender queue. Receivers will signal when they take data. + var sender = Sender{ .data = data }; + + // prime condition + sender.mut.lock(); // cond.wait below will unlock it and wait until signal, then relock it + defer sender.mut.unlock(); // unlocks the relock + + try self.sendQ.append(&sender); // make visible to other threads + self.mut.unlock(); // allow all other threads to proceed. This thread is done reading/writing + + // now just wait for receiver to signal sender + sender.cond.wait(&sender.mut); + return; + } + + pub fn recv(self: *Self) ChanError!T { + if (self.closed) return ChanError.Closed; + self.mut.lock(); + errdefer self.mut.unlock(); + + // case: value in buffer + const l = self.len(); + if (l > 0 and bufSize > 0) { + defer self.mut.unlock(); + const val = self.buf[0] orelse return ChanError.DataCorruption; + + // advance items in buffer + if (l > 1) { + for (self.buf[1..l], 0..l - 1) |item, i| { + self.buf[i] = item; + } + } + self.buf[l - 1] = null; + + // top up buffer with a waiting sender, if any + if (self.sendQ.items.len > 0) { + var sender: *Sender = self.sendQ.orderedRemove(0); + const valFromSender: T = sender.getDataAndSignal(); + self.buf[l - 1] = valFromSender; + } + + return val; + } + + // case: sender already waiting + // pull sender and take its data. Signal sender that it's done waiting. + if (self.sendQ.items.len > 0) { + defer self.mut.unlock(); + var sender: *Sender = self.sendQ.orderedRemove(0); + const data: T = sender.getDataAndSignal(); + return data; + } + + // hold on receiver queue. Senders will signal when they take it. + var receiver = Receiver{}; + + // prime condition + receiver.mut.lock(); + defer receiver.mut.unlock(); + + try self.recvQ.append(&receiver); + self.mut.unlock(); + + // now wait for sender to signal receiver + receiver.cond.wait(&receiver.mut); + // sender should have put data in .data + if (receiver.data) |data| { + return data; + } else { + return ChanError.DataCorruption; + } + } + }; +} + +test "unbufferedChan" { + // create channel of u8 + const T = Chan(u8); + var chan = T.init(std.testing.allocator); + defer chan.deinit(); + + // spawn thread that immediately waits on channel + const thread = struct { + fn func(c: *T) !void { + const val = try c.recv(); + std.debug.print("{d} Thread Received {d}\n", .{ std.time.milliTimestamp(), val }); + } + }; + const t = try std.Thread.spawn(.{}, thread.func, .{&chan}); + defer t.join(); + + // let thread wait a bit before sending value + std.time.sleep(1_000_000_000); + + const val: u8 = 10; + std.debug.print("{d} Main Sending {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); +} + +test "bidirectional unbufferedChan" { + std.debug.print("\n", .{}); + + const T = Chan(u8); + var chan = T.init(std.testing.allocator); + defer chan.deinit(); + + const thread = struct { + fn func(c: *T) !void { + std.time.sleep(2_000_000_000); + const val = try c.recv(); + std.debug.print("{d} Thread Received {d}\n", .{ std.time.milliTimestamp(), val }); + std.time.sleep(1_000_000_000); + std.debug.print("{d} Thread Sending {d}\n", .{ std.time.milliTimestamp(), val + 1 }); + try c.send(val + 1); + std.time.sleep(2_000_000_000); + std.debug.print("{d} Thread Sending {d}\n", .{ std.time.milliTimestamp(), val + 100 }); + try c.send(val + 100); + std.debug.print("{d} Thread Exit\n", .{std.time.milliTimestamp()}); + } + }; + + const t = try std.Thread.spawn(.{}, thread.func, .{&chan}); + defer t.join(); + + std.time.sleep(1_000_000_000); + var val: u8 = 10; + std.debug.print("{d} Main Sending {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); + val = try chan.recv(); + std.debug.print("{d} Main Received {d}\n", .{ std.time.milliTimestamp(), val }); + val = try chan.recv(); + std.debug.print("{d} Main Received {d}\n", .{ std.time.milliTimestamp(), val }); +} + +test "buffered Chan" { + std.debug.print("\n", .{}); + + const T = BufferedChan(u8, 3); + var chan = T.init(std.testing.allocator); + defer chan.deinit(); + + const thread = struct { + fn func(c: *T) !void { + std.time.sleep(2_000_000_000); + std.debug.print("{d} Thread Receiving\n", .{std.time.milliTimestamp()}); + var val = try c.recv(); + std.debug.print("{d} Thread Received {d}\n", .{ std.time.milliTimestamp(), val }); + std.time.sleep(1_000_000_000); + std.debug.print("{d} Thread Receiving\n", .{std.time.milliTimestamp()}); + val = try c.recv(); + std.debug.print("{d} Thread Received {d}\n", .{ std.time.milliTimestamp(), val }); + std.time.sleep(1_000_000_000); + std.debug.print("{d} Thread Receiving\n", .{std.time.milliTimestamp()}); + val = try c.recv(); + std.debug.print("{d} Thread Received {d}\n", .{ std.time.milliTimestamp(), val }); + std.time.sleep(1_000_000_000); + std.debug.print("{d} Thread Receiving\n", .{std.time.milliTimestamp()}); + val = try c.recv(); + std.debug.print("{d} Thread Received {d}\n", .{ std.time.milliTimestamp(), val }); + } + }; + + const t = try std.Thread.spawn(.{}, thread.func, .{&chan}); + defer t.join(); + + std.time.sleep(1_000_000_000); + var val: u8 = 10; + std.debug.print("{d} Main Sending {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); + std.debug.print("{d} Main Sent {d}\n", .{ std.time.milliTimestamp(), val }); + + val = 11; + std.debug.print("{d} Main Sending {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); + std.debug.print("{d} Main Sent {d}\n", .{ std.time.milliTimestamp(), val }); + + val = 12; + std.debug.print("{d} Main Sending {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); + std.debug.print("{d} Main Sent {d}\n", .{ std.time.milliTimestamp(), val }); + + val = 13; + std.debug.print("{d} Main Sending {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); + std.debug.print("{d} Main Sent {d}\n", .{ std.time.milliTimestamp(), val }); +} + +test "chan of chan" { + std.debug.print("\n", .{}); + + const T = BufferedChan(u8, 3); + const TofT = Chan(T); + var chanOfChan = TofT.init(std.testing.allocator); + defer chanOfChan.deinit(); + + const thread = struct { + fn func(cOC: *TofT) !void { + std.time.sleep(2_000_000_000); + std.debug.print("{d} Thread Receiving\n", .{std.time.milliTimestamp()}); + var c = try cOC.recv(); + std.debug.print("{d} Thread Received chan of chan: {any}\n", .{ std.time.milliTimestamp(), cOC }); + std.debug.print("{d} Thread pulling from chan buffer\n", .{std.time.milliTimestamp()}); + var val = try c.recv(); // should have value on buffer + std.debug.print("{d} Thread received from chan: {d}\n", .{ std.time.milliTimestamp(), val }); + } + }; + + const t = try std.Thread.spawn(.{}, thread.func, .{&chanOfChan}); + defer t.join(); + + std.time.sleep(1_000_000_000); + var val: u8 = 10; + var chan = T.init(std.testing.allocator); + defer chan.deinit(); + std.debug.print("{d} Main sending u8 to chan {d}\n", .{ std.time.milliTimestamp(), val }); + try chan.send(val); + + std.debug.print("{d} Main sending chan across chanOfChan\n", .{std.time.milliTimestamp()}); + try chanOfChan.send(chan); +} diff --git a/src/core/lightning/lightning.zig b/src/core/lightning/lightning.zig index 4008420..530659d 100644 --- a/src/core/lightning/lightning.zig +++ b/src/core/lightning/lightning.zig @@ -1,5 +1,7 @@ //! Mint Lightning const root = @import("../../lib.zig"); +const std = @import("std"); + const Bolt11Invoice = root.lightning_invoices.Bolt11Invoice; const Amount = root.core.amount.Amount; const MeltQuoteState = root.core.nuts.nut05.QuoteState; @@ -25,6 +27,12 @@ pub const PayInvoiceResponse = struct { status: MeltQuoteState, /// Totoal Amount Spent total_spent: Amount, + + pub fn deinit(self: PayInvoiceResponse, allocator: std.mem.Allocator) void { + allocator.free(self.payment_hash); + + if (self.payment_preimage) |p| allocator.free(p); + } }; /// Payment quote response @@ -35,6 +43,10 @@ pub const PaymentQuoteResponse = struct { amount: Amount, /// Fee required for melt fee: u64, + + pub fn deinit(self: PaymentQuoteResponse, allocator: std.mem.Allocator) void { + allocator.free(self.request_lookup_id); + } }; /// Ln backend settings diff --git a/src/core/mint/mint.zig b/src/core/mint/mint.zig index e0db3c0..62bc456 100644 --- a/src/core/mint/mint.zig +++ b/src/core/mint/mint.zig @@ -1,10 +1,20 @@ const std = @import("std"); const core = @import("../lib.zig"); -const MintInfo = core.nuts.MintInfo; const secp256k1 = @import("secp256k1"); const bip32 = @import("bitcoin").bitcoin.bip32; +const helper = @import("../../helper/helper.zig"); +const nuts = core.nuts; + +const RWMutex = helper.RWMutex; +const MintInfo = core.nuts.MintInfo; +const MintQuoteBolt11Response = core.nuts.nut04.MintQuoteBolt11Response; +const MintQuoteState = core.nuts.nut04.QuoteState; + pub const MintQuote = @import("types.zig").MintQuote; pub const MeltQuote = @import("types.zig").MeltQuote; +pub const MintMemoryDatabase = core.mint_memory.MintMemoryDatabase; + +// TODO implement tests /// Mint Fee Reserve pub const FeeReserve = struct { @@ -61,9 +71,72 @@ pub const Mint = struct { /// Mint Info mint_info: MintInfo, /// Mint Storage backend - // pub localstore: Arc + Send + Sync>, + localstore: helper.RWMutex(MintMemoryDatabase), /// Active Mint Keysets - // keysets: Arc>>, + keysets: RWMutex(std.AutoHashMap(nuts.Id, nuts.MintKeySet)), secp_ctx: secp256k1.Secp256k1, xpriv: bip32.ExtendedPrivKey, + + /// Creating new [`MintQuote`], all arguments are cloned and reallocated + /// caller responsible on free resources of result + pub fn newMintQuote( + self: *Mint, + allocator: std.mem.Allocator, + mint_url: []const u8, + request: []const u8, + unit: nuts.CurrencyUnit, + amount: core.amount.Amount, + expiry: u64, + ln_lookup: []const u8, + ) !MintQuote { + const nut04 = self.mint_info.nuts.nut04; + if (nut04.disabled) return error.MintingDisabled; + + if (nut04.getSettings(unit, .bolt11)) |settings| { + if (settings.max_amount) |max_amount| if (amount > max_amount) return error.MintOverLimit; + + if (settings.min_amount) |min_amount| if (amount < min_amount) return error.MintUnderLimit; + } else return error.UnsupportedUnit; + + const quote = try MintQuote.initAlloc(allocator, mint_url, request, unit, amount, expiry, ln_lookup); + errdefer quote.deinit(allocator); + + std.log.debug("New mint quote: {any}", .{quote}); + + self.localstore.lock.lock(); + + defer self.localstore.lock.unlock(); + try self.localstore.value.addMintQuote(quote); + + return quote; + } + + /// Check mint quote + /// caller own result and should deinit + pub fn checkMintQuote(self: *Mint, allocator: std.mem.Allocator, quote_id: [16]u8) !MintQuoteBolt11Response { + const quote = v: { + self.localstore.lock.lockShared(); + defer self.localstore.lock.unlockShared(); + break :v (try self.localstore.value.getMintQuote(allocator, quote_id)) orelse return error.UnknownQuote; + }; + defer quote.deinit(allocator); + + const paid = quote.state == .paid; + + // Since the pending state is not part of the NUT it should not be part of the response. + // In practice the wallet should not be checking the state of a quote while waiting for the mint response. + const state = switch (quote.state) { + .pending => MintQuoteState.paid, + else => quote.state, + }; + + const result = MintQuoteBolt11Response{ + .quote = "e.id, + .request = quote.request, + .paid = paid, + .state = state, + .expiry = quote.expiry, + }; + return try result.clone(allocator); + } }; diff --git a/src/core/mint/types.zig b/src/core/mint/types.zig index 16d501d..c249331 100644 --- a/src/core/mint/types.zig +++ b/src/core/mint/types.zig @@ -11,7 +11,7 @@ pub const MintQuote = struct { /// Quote id id: [16]u8, /// Mint Url - mint_url: std.Uri, + mint_url: []const u8, /// Amount of quote amount: amount_lib.Amount, /// Unit of quote @@ -26,17 +26,19 @@ pub const MintQuote = struct { request_lookup_id: []const u8, /// Create new [`MintQuote`] - pub fn init( - mint_url: std.Uri, + /// creating copy of arguments, so caller responsible on deinit resources + pub fn initAlloc( + allocator: std.mem.Allocator, + mint_url: []const u8, request: []const u8, unit: CurrencyUnit, amount: amount_lib.Amount, expiry: u64, request_lookup_id: []const u8, - ) MintQuote { + ) !MintQuote { const id = zul.UUID.v4(); - return .{ + const mint_quote: MintQuote = .{ .mint_url = mint_url, .id = id.bin, .amount = amount, @@ -46,6 +48,8 @@ pub const MintQuote = struct { .expiry = expiry, .request_lookup_id = request_lookup_id, }; + + return try mint_quote.clone(allocator); } pub fn deinit(self: *const MintQuote, allocator: std.mem.Allocator) void { @@ -54,15 +58,20 @@ pub const MintQuote = struct { } pub fn clone(self: *const MintQuote, allocator: std.mem.Allocator) !MintQuote { - const request_lookup = try allocator.alloc(u8, self.request_lookup_id.len); + const request_lookup = try allocator.dupe(u8, self.request_lookup_id); errdefer allocator.free(request_lookup); - const request = try allocator.alloc(u8, self.request.len); + const request = try allocator.dupe(u8, self.request); errdefer allocator.free(request); + const mint_url = try allocator.dupe(u8, self.mint_url); + errdefer allocator.free(mint_url); + var cloned = self.*; + cloned.request = request; cloned.request_lookup_id = request_lookup; + cloned.mint_url = mint_url; return cloned; } diff --git a/src/core/nuts/nut04/nut04.zig b/src/core/nuts/nut04/nut04.zig index b059d4d..59b0b7c 100644 --- a/src/core/nuts/nut04/nut04.zig +++ b/src/core/nuts/nut04/nut04.zig @@ -5,6 +5,7 @@ const std = @import("std"); const CurrencyUnit = @import("../nut00/nut00.zig").CurrencyUnit; const Proof = @import("../nut00/nut00.zig").Proof; const PaymentMethod = @import("../nut00/nut00.zig").PaymentMethod; +const MintQuote = @import("../../mint/types.zig").MintQuote; pub const QuoteState = enum { /// Quote has not been paid @@ -16,6 +17,15 @@ pub const QuoteState = enum { pending, /// ecash issued for quote issued, + + pub fn fromStr(s: []const u8) !QuoteState { + if (std.mem.eql(u8, "UNPAID", s)) return .unpaid; + if (std.mem.eql(u8, "PAID", s)) return .paid; + if (std.mem.eql(u8, "PENDING", s)) return .pending; + if (std.mem.eql(u8, "ISSUED", s)) return .issued; + + return error.UnknownState; + } }; pub const MintMethodSettings = struct { @@ -35,4 +45,125 @@ pub const Settings = struct { methods: []const MintMethodSettings = &.{}, /// Minting disabled disabled: bool = false, + + /// Get [`MintMethodSettings`] for unit method pair + pub fn getSettings( + self: Settings, + unit: CurrencyUnit, + method: PaymentMethod, + ) ?MintMethodSettings { + for (self.methods) |method_settings| { + if (std.meta.eql(method_settings.method, method) and std.meta.eql(method_settings.unit, unit)) return method_settings; + } + + return null; + } +}; + +/// Mint quote response [NUT-04] +pub const MintQuoteBolt11Response = struct { + /// Quote Id + quote: []const u8, + /// Payment request to fulfil + request: []const u8, + // TODO: To be deprecated + /// Whether the the request haas be paid + /// Deprecated + paid: ?bool, + /// Quote State + state: QuoteState, + /// Unix timestamp until the quote is valid + expiry: ?u64, + + pub fn clone(self: *const @This(), allocator: std.mem.Allocator) !@This() { + const quote = try allocator.dupe(u8, self.quote); + errdefer allocator.free(quote); + + const request = try allocator.dupe(u8, self.request); + errdefer allocator.free(request); + + var cloned = self.*; + cloned.request = request; + cloned.quote = quote; + return cloned; + } + + pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void { + allocator.free(self.request); + allocator.free(self.request); + } + + /// Without reallocating slices, so lifetime of result as [`MintQuote`] + pub fn fromMintQuote(mint_quote: MintQuote) !MintQuoteBolt11Response { + const paid = mint_quote.state == .paid; + return .{ + .quote = &mint_quote.id, + .request = mint_quote.request, + .paid = paid, + .state = mint_quote.state, + .expiry = mint_quote.expiry, + }; + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !MintQuoteBolt11Response { + const value = std.json.innerParse(std.json.Value, allocator, source, .{ .allocate = .alloc_always }) catch return error.UnexpectedToken; + + if (value != .object) return error.UnexpectedToken; + + const quote: []const u8 = try std.json.parseFromValueLeaky( + []const u8, + allocator, + value.object.get("quote") orelse return error.UnexpectedToken, + options, + ); + + const request: []const u8 = try std.json.parseFromValueLeaky( + []const u8, + allocator, + value.object.get("request") orelse return error.UnexpectedToken, + options, + ); + + const paid: ?bool = v: { + break :v try std.json.parseFromValueLeaky( + bool, + allocator, + value.object.get("paid") orelse break :v null, + options, + ); + }; + + const state: ?[]const u8 = v: { + break :v try std.json.parseFromValueLeaky( + []const u8, + allocator, + value.object.get("state") orelse break :v null, + options, + ); + }; + const expiry: ?u64 = v: { + break :v try std.json.parseFromValueLeaky( + []u64, + allocator, + value.object.get("expiry") orelse break :v null, + options, + ); + }; + + const _state: QuoteState = if (state) |s| + // wrong quote state + QuoteState.fromStr(s) catch error.UnexpectedToken + else if (paid) |p| + if (p) .paid else .unpaid + else + return error.UnexpectedError; + + return .{ + .state = _state, + .expiry = expiry, + .request = request, + .quote = quote, + .paid = paid, + }; + } }; diff --git a/src/fake_wallet/fake_wallet.zig b/src/fake_wallet/fake_wallet.zig index ae91a9d..66b04e1 100644 --- a/src/fake_wallet/fake_wallet.zig +++ b/src/fake_wallet/fake_wallet.zig @@ -2,13 +2,130 @@ //! //! Used for testing where quotes are auto filled const core = @import("../core/lib.zig"); -// const lightning = @import("../core/mint/lightning/lib.zig"). +const std = @import("std"); +const lightning_invoice = @import("../lightning_invoices/invoice.zig"); +const helper = @import("../helper/helper.zig"); + +const Amount = core.amount.Amount; +const PaymentQuoteResponse = core.lightning.PaymentQuoteResponse; +const PayInvoiceResponse = core.lightning.PayInvoiceResponse; +const MeltQuoteBolt11Request = core.nuts.nut05.MeltQuoteBolt11Request; +const Settings = core.lightning.Settings; +const MintMeltSettings = core.lightning.MintMeltSettings; +const FeeReserve = core.mint.FeeReserve; +const Channel = @import("channels").Chan; +const MintQuoteState = core.nuts.nut04.QuoteState; + +// TODO: wait any invoices, here we need create a new listener, that will receive +// message like pub sub channel /// Fake Wallet pub const FakeWallet = struct { + const Self = @This(); + fee_reserve: core.mint.FeeReserve, - // sender: tokio::sync::mpsc::Sender, - // receiver: Arc>>>, - // mint_settings: MintMeltSettings, - // melt_settings: MintMeltSettings, + chan: Channel([]const u8) = .{}, // TODO + mint_settings: MintMeltSettings, + melt_settings: MintMeltSettings, + + /// Creat new [`FakeWallet`] + pub fn new( + fee_reserve: FeeReserve, + mint_settings: MintMeltSettings, + melt_settings: MintMeltSettings, + ) !FakeWallet { + return .{ + .fee_reserve = fee_reserve, + .mint_settings = mint_settings, + .melt_settings = melt_settings, + }; + } + + pub fn getSettings(self: *const Self) Settings { + return .{ + .mpp = true, + .unit = .msat, + .melt_settings = self.mel_settings, + .mint_settings = self.mint_settings, + }; + } + + // Result is channel with invoices, caller must free result + pub fn waitAnyInvoice( + self: *const Self, + allocator: std.mem.Allocator, + ) !Channel([]const u8) { + _ = self; // autofix + return Channel([]const u8).init(allocator); + } + + /// caller responsible to deallocate result + pub fn getPaymentQuote( + self: *const Self, + allocator: std.mem.Allocator, + melt_quote_request: MeltQuoteBolt11Request, + ) !PaymentQuoteResponse { + const invoice_amount_msat = melt_quote_request + .request + .amountMilliSatoshis() orelse return error.UnknownInvoiceAmount; + + const amount = try core.lightning.toUnit( + invoice_amount_msat, + .msat, + melt_quote_request.unit, + ); + + const relative_fee_reserve: u64 = + @intFromFloat(@as(f32, @floatFromInt(self.fee_reserve.percent_fee_reserve * amount))); + + const absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve; + + const fee = if (relative_fee_reserve > absolute_fee_reserve) + relative_fee_reserve + else + absolute_fee_reserve; + + const req_lookup_id = try helper.copySlice(allocator, &melt_quote_request.request.paymentHash()); + errdefer allocator.free(req_lookup_id); + + return .{ + .request_lookup_id = req_lookup_id, + .amount = amount, + .fee = fee, + }; + } + + /// pay invoice, caller responsible too free + pub fn payInvoice( + self: *const Self, + allocator: std.mem.Allocator, + melt_quote: core.mint.MeltQuote, + _partial_msats: ?Amount, + _max_fee_msats: ?Amount, + ) !PayInvoiceResponse { + _ = allocator; // autofix + _ = self; // autofix + _ = _partial_msats; // autofix + _ = _max_fee_msats; // autofix + + return .{ + .payment_preimage = &.{}, + .payment_hash = &.{}, // empty slice - safe to free + .status = .paid, + .total_spend = melt_quote.amount, + }; + } + + pub fn checkInvoiceStatus( + self: *const Self, + _request_lookup_id: []const u8, + ) !MintQuoteState { + _ = self; // autofix + _ = _request_lookup_id; // autofix + return .paid; + } + + // create_invoice TODO after implementing PubSub/REST Server }; + +test {} diff --git a/src/fake_wallet/pub_sub.zig b/src/fake_wallet/pub_sub.zig new file mode 100644 index 0000000..62950b5 --- /dev/null +++ b/src/fake_wallet/pub_sub.zig @@ -0,0 +1 @@ +// TODO: here we need implement PubSub struct thread-safe diff --git a/src/helper/helper.zig b/src/helper/helper.zig index 634501e..ec223a9 100644 --- a/src/helper/helper.zig +++ b/src/helper/helper.zig @@ -1,5 +1,20 @@ const std = @import("std"); +// TODO add atomic ref count? +pub fn RWMutex(comptime T: type) type { + return struct { + value: T, + lock: std.Thread.RwLock, + }; +} + +pub inline fn copySlice(allocator: std.mem.Allocator, slice: []const u8) ![]u8 { + const allocated = try allocator.alloc(u8, slice.len); + + @memcpy(allocated, slice); + return allocated; +} + pub fn Parsed(comptime T: type) type { return struct { arena: *std.heap.ArenaAllocator,