diff --git a/.gitignore b/.gitignore index 0133c8b..df2ad55 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +# exclude all db +*.sqlite + # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/build.zig b/build.zig index 16bcc1c..609d0af 100644 --- a/build.zig +++ b/build.zig @@ -29,8 +29,42 @@ const external_dependencies = [_]build_helpers.Dependency{ .name = "clap", .module_name = "clap", }, + .{ + .name = "zqlite", + .module_name = "zqlite", + }, }; +fn installSqliteDependency(sqlitec: *std.Build.Dependency, compile: *std.Build.Step.Compile) void { + compile.addCSourceFile(.{ + .file = sqlitec.path("sqlite3.c"), + .flags = &[_][]const u8{ + "-DSQLITE_DQS=0", + "-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1", + "-DSQLITE_USE_ALLOCA=1", + "-DSQLITE_THREADSAFE=1", + "-DSQLITE_TEMP_STORE=3", + "-DSQLITE_ENABLE_API_ARMOR=1", + "-DSQLITE_ENABLE_UNLOCK_NOTIFY", + "-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1", + "-DSQLITE_DEFAULT_FILE_PERMISSIONS=0600", + "-DSQLITE_OMIT_DECLTYPE=1", + "-DSQLITE_OMIT_DEPRECATED=1", + "-DSQLITE_OMIT_LOAD_EXTENSION=1", + "-DSQLITE_OMIT_PROGRESS_CALLBACK=1", + "-DSQLITE_OMIT_SHARED_CACHE", + "-DSQLITE_OMIT_UTF16=1", + "-DHAVE_USLEEP=0", + "-DSQLITE_DEBUG=1", + "-DSQLITE_ENABLE_EXPLAIN_COMMENTS=1", + "-DSQLITE_ENABLE_TREETRACE=1", + "-DSQLITE_ENABLE_WHERETRACE=1", + "-DSQLITE_TRACE_STMT=1", + }, + }); + compile.linkLibC(); +} + pub fn build(b: *std.Build) !void { // Standard target options allows the person running `zig build` to choose // what target to build for. Here we do not override the defaults, which @@ -43,6 +77,12 @@ pub fn build(b: *std.Build) !void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); + // Sqlite3 c library source code + const sqlitec = b.dependency("sqlitec", .{ + .target = target, + .optimize = optimize, + }); + // ************************************************************** // * HANDLE DEPENDENCY MODULES * // ************************************************************** @@ -101,6 +141,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }); + installSqliteDependency(sqlitec, exe); // Add dependency modules to the library. for (deps) |mod| exe.root_module.addImport( @@ -124,6 +165,7 @@ pub fn build(b: *std.Build) !void { mod.name, mod.module, ); + installSqliteDependency(sqlitec, exe); check.dependOn(&exe.step); } @@ -136,6 +178,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, .single_threaded = false, }); + installSqliteDependency(sqlitec, lib_unit_tests); // Add dependency modules to the library. for (deps) |mod| lib_unit_tests.root_module.addImport( @@ -160,6 +203,8 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); + installSqliteDependency(sqlitec, exe); + // Add dependency modules to the library. for (deps) |mod| exe.root_module.addImport( mod.name, @@ -190,6 +235,8 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); + installSqliteDependency(sqlitec, exe); + // Add dependency modules to the library. for (deps) |mod| exe.root_module.addImport( mod.name, @@ -215,6 +262,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, .single_threaded = false, }); + installSqliteDependency(sqlitec, lib_unit_tests); // Add dependency modules to the library. for (deps) |mod| lib_unit_tests.root_module.addImport( diff --git a/build.zig.zon b/build.zig.zon index 459be69..19c5cc7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -24,8 +24,8 @@ // internet connectivity. .dependencies = .{ .zul = .{ - .url = "https://github.com/karlseguin/zul/archive/08c989bf6871e87807a4668232913ee245425863.zip", - .hash = "12206f5d1e5bd4793fe952bbae891b7424a19026e0d296a1381074c7d21d5d76c1a1", + .url = "https://github.com/StringNick/zul/archive/9f9acdff296918e0f81e8af39b269dad988c48f9.zip", + .hash = "122016552e83ec129489fd32d89b4122f737fcdacb22dbee34dca2fbc62981bb487d", }, .@"zig-cli" = .{ .url = "https://github.com/StringNick/zig-cli/archive/c9b9d17b14c524785a32a5b7c930b9584a331372.zip", @@ -51,6 +51,18 @@ .url = "git+https://github.com/Hejsil/zig-clap#2d9db156ae928860a9acf2f1260750d3b44a4c98", .hash = "122005e589ab3b6bff8e589b45f5b12cd27ce79f266bdac17e9f33ebfe2fbaff7fe3", }, + .rocksdb = .{ + .url = "git+https://github.com/zig-bitcoin/rocksdb-zig#27f69e3756999ac711f4f9278a698fd5eecce169", + .hash = "122086b7931284e391223ce165a9bee94b624a7fb34531ecc95cafb22f049887d175", + }, + .zqlite = .{ + .url = "git+https://github.com/karlseguin/zqlite.zig#5b9e8715fb174c1c1f0bf15e4fbf5e77df5c6b34", + .hash = "12209446c0581a04f460f978c489c6e5eec62ca1c4f95a7c5cf8f1d2abc597074db7", + }, + .sqlitec = .{ + .url = "https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip", + .hash = "12206bc219bf56b927469c7d43f53c23fe2801c8158800f8613a783956998b7fc37f", + }, }, .paths = .{ "build.zig", diff --git a/src/core/database/database.zig b/src/core/database/database.zig index b7fda35..9494b06 100644 --- a/src/core/database/database.zig +++ b/src/core/database/database.zig @@ -11,15 +11,13 @@ const MintQuote = @import("../mint/mint.zig").MintQuote; const MeltQuote = @import("../mint/mint.zig").MeltQuote; pub const MintMemoryDatabase = @import("mint_memory.zig").MintMemoryDatabase; +pub const MintSqliteDatabase = @import("mint_sqlite.zig").Database; pub const MintDatabase = struct { const Self = @This(); allocator: std.mem.Allocator, ptr: *anyopaque, - size: usize, - align_of: usize, - deinitFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator) void, setActiveKeysetFn: *const fn (ptr: *anyopaque, unit: nuts.CurrencyUnit, id: nuts.Id) anyerror!void, @@ -70,6 +68,7 @@ pub const MintDatabase = struct { ptr: *anyopaque, blinded_messages: []const secp256k1.PublicKey, blind_signatures: []const nuts.BlindSignature, + quote_id: ?[]const u8, ) anyerror!void, getBlindSignaturesFn: *const fn ( ptr: *anyopaque, @@ -218,9 +217,10 @@ pub const MintDatabase = struct { pointer: *anyopaque, blinded_messages: []const secp256k1.PublicKey, blind_signatures: []const nuts.BlindSignature, + quote_id: ?[]const u8, ) anyerror!void { const self: *T = @ptrCast(@alignCast(pointer)); - return self.addBlindSignatures(blinded_messages, blind_signatures); + return self.addBlindSignatures(blinded_messages, blind_signatures, quote_id); } pub fn getBlindSignatures( @@ -240,14 +240,15 @@ pub const MintDatabase = struct { const self: *T = @ptrCast(@alignCast(pointer)); return self.getBlindSignaturesForKeyset(gpa, keyset_id); } - pub fn deinit(pointer: *anyopaque, allocator: std.mem.Allocator) void { + + pub fn deinit(pointer: *anyopaque, __allocator: std.mem.Allocator) void { const self: *T = @ptrCast(@alignCast(pointer)); if (std.meta.hasFn(T, "deinit")) { self.deinit(); } - allocator.destroy(self); + __allocator.destroy(self); } }; @@ -257,8 +258,6 @@ pub const MintDatabase = struct { return .{ .ptr = ptr, .allocator = _allocator, - .size = @sizeOf(T), - .align_of = @alignOf(T), .getBlindSignaturesForKeysetFn = gen.getBlindSignaturesForKeyset, @@ -406,8 +405,9 @@ pub const MintDatabase = struct { self: Self, blinded_messages: []const secp256k1.PublicKey, blind_signatures: []const nuts.BlindSignature, + quote_id: ?[]const u8, ) anyerror!void { - return self.addBlindSignaturesFn(self.ptr, blinded_messages, blind_signatures); + return self.addBlindSignaturesFn(self.ptr, blinded_messages, blind_signatures, quote_id); } pub fn getBlindSignatures( diff --git a/src/core/database/mint_memory.zig b/src/core/database/mint_memory.zig index e1b48a4..75c3716 100644 --- a/src/core/database/mint_memory.zig +++ b/src/core/database/mint_memory.zig @@ -540,13 +540,17 @@ pub const MintMemoryDatabase = struct { self: *Self, blinded_messages: []const secp256k1.PublicKey, blind_signatures: []const nuts.BlindSignature, + quote_id: ?[]const u8, ) !void { + _ = quote_id; // autofix self.lock.lock(); defer self.lock.unlock(); for (blinded_messages, blind_signatures) |blinded_message, blind_signature| { try self.blinded_signatures.put(blinded_message.serialize(), blind_signature); } + + // TODO quote signatures } pub fn getBlindSignatures( diff --git a/src/core/database/mint_sqlite.zig b/src/core/database/mint_sqlite.zig new file mode 100644 index 0000000..8404e7a --- /dev/null +++ b/src/core/database/mint_sqlite.zig @@ -0,0 +1,769 @@ +const std = @import("std"); +const nuts = @import("../nuts/lib.zig"); +const dhke = @import("../dhke.zig"); +const zul = @import("zul"); +const bitcoin_primitives = @import("bitcoin-primitives"); +const secp256k1 = bitcoin_primitives.secp256k1; +const sqlite = @import("zqlite"); +const secret = @import("../secret.zig"); + +const Arened = @import("../../helper/helper.zig").Parsed; +const MintKeySetInfo = @import("../mint/mint.zig").MintKeySetInfo; +const MintQuote = @import("../mint/mint.zig").MintQuote; +const MeltQuote = @import("../mint/mint.zig").MeltQuote; + +/// Executes ones, on first connection, this like migration +fn initializeDB(conn: sqlite.Conn) !void { + try conn.transaction(); + errdefer conn.rollback(); + errdefer std.log.debug("{any}", .{@errorReturnTrace()}); + + std.log.debug("initializing database", .{}); + try conn.busyTimeout(1000); + const sql = + \\CREATE TABLE if not exists active_keysets ( currency_unit INTEGER PRIMARY KEY, id BLOB); + \\CREATE TABLE if not exists keysets ( id BLOB PRIMARY KEY, keyset_info JSONB); + \\CREATE TABLE if not exists melt_quote ( id BLOB PRIMARY KEY, quote JSONB); + \\CREATE TABLE if not exists mint_quote ( id BLOB PRIMARY KEY, quote JSONB); + \\ CREATE TABLE IF NOT EXISTS proof ( + \\ y BLOB PRIMARY KEY, + \\ amount INTEGER NOT NULL, + \\ keyset_id TEXT NOT NULL, + \\ secret TEXT NOT NULL, + \\ c BLOB NOT NULL, + \\ witness TEXT, + \\ state TEXT CHECK ( state IN ('SPENT', 'UNSPENT', 'PENDING', 'RESERVED' ) ) NOT NULL + \\ ); + \\ + \\ CREATE INDEX IF NOT EXISTS state_index ON proof(state); + \\ CREATE INDEX IF NOT EXISTS secret_index ON proof(secret); + \\CREATE TABLE if not exists proof_states ( id BLOB PRIMARY KEY, proof INTEGER); + \\CREATE TABLE if not exists blind_signatures ( id BLOB PRIMARY KEY, blind_signature BLOB); + \\ + \\ CREATE TABLE IF NOT EXISTS blind_signature ( + \\ y BLOB PRIMARY KEY, + \\ amount INTEGER NOT NULL, + \\ keyset_id TEXT NOT NULL, + \\ quote_id TEXT, + \\ c BLOB NOT NULL + \\ ); + \\ + \\ CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id); + ; + + try conn.execNoArgs(sql); + try conn.commit(); +} + +/// TODO simple solution for rw locks, use on all structure, as temp solution +/// Mint Memory Database +pub const Database = struct { + const Self = @This(); + + allocator: std.mem.Allocator, + pool: sqlite.Pool, + + pub fn deinit(self: *Database) void { + self.pool.deinit(); + } + + /// initFrom - take own on all data there, except slices (only own data in slices) + pub fn initFrom( + allocator: std.mem.Allocator, + path: [*:0]const u8, + ) !Database { + const pool = try sqlite.Pool.init(allocator, .{ + .size = 5, + .flags = sqlite.OpenFlags.Create | sqlite.OpenFlags.EXResCode, + .on_first_connection = &initializeDB, + .path = path, + }); + + return .{ + .pool = pool, + .allocator = allocator, + }; + } + + pub fn setActiveKeyset(self: *Self, unit: nuts.CurrencyUnit, id: nuts.Id) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + try conn.exec("INSERT INTO active_keysets (currency_unit, id) VALUES (?1, ?2) ON CONFLICT DO UPDATE SET id = ?2;", .{ @intFromEnum(unit), sqlite.blob(&id.toString()) }); + } + + pub fn getActiveKeysetId(self: *Self, unit: nuts.CurrencyUnit) ?nuts.Id { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + if (conn.row("SELECT id FROM active_keysets WHERE currency_unit = ?1", .{@intFromEnum(unit)}) catch return null) |row| { + defer row.deinit(); // must be called + const id_blob = row.blob(0); + + const id = nuts.Id.fromStr(id_blob) catch unreachable; + + return id; + } + + return null; + } + + /// caller own result data, so responsible to deallocate + pub fn getActiveKeysets(self: *Self, allocator: std.mem.Allocator) !std.AutoHashMap(nuts.CurrencyUnit, nuts.Id) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var rows = try conn.rows("SELECT id, currency_unit FROM active_keysets", .{}); + defer rows.deinit(); + + var result = std.AutoHashMap(nuts.CurrencyUnit, nuts.Id).init(allocator); + errdefer result.deinit(); + + while (rows.next()) |row| { + const id_blob = row.blob(0); + + const id = nuts.Id.fromStr(id_blob) catch unreachable; + const unit: nuts.CurrencyUnit = @enumFromInt(row.int(1)); + + try result.put(unit, id); + } + + return result; + } + + /// keyset inside is cloned, so caller own keyset + pub fn addKeysetInfo(self: *Self, keyset: MintKeySetInfo) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + const keyset_encoded = try std.json.stringifyAlloc(self.allocator, keyset, .{}); + defer self.allocator.free(keyset_encoded); + + try conn.exec( + \\INSERT INTO keysets (id, keyset_info) + \\VALUES (?1, ?2) + \\ ON CONFLICT (id) DO UPDATE SET keyset_info=?2; + , .{ + sqlite.blob(&keyset.id.toBytes()), + sqlite.blob(keyset_encoded), + }); + } + + /// caller own result, so responsible to free + pub fn getKeysetInfo(self: *Self, allocator: std.mem.Allocator, keyset_id: nuts.Id) !?MintKeySetInfo { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + if (try conn.row("SELECT keyset_info FROM keysets WHERE id = ?1", .{ + sqlite.blob(&keyset_id.toBytes()), + })) |row| { + defer row.deinit(); // must be called + const keyset_info_encoded = row.blob(0); + + const decoded_ks_info = try std.json.parseFromSlice(MintKeySetInfo, self.allocator, keyset_info_encoded, .{}); + defer decoded_ks_info.deinit(); + + return try decoded_ks_info.value.clone(allocator); + } + + return null; + } + + pub fn getKeysetInfos(self: *Self, allocator: std.mem.Allocator) !Arened(std.ArrayList(MintKeySetInfo)) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var res = try Arened(std.ArrayList(MintKeySetInfo)).init(allocator); + errdefer res.deinit(); + + res.value = std.ArrayList(MintKeySetInfo).init(res.arena.allocator()); + + var rows = try conn.rows("SELECT id, keyset_info FROM keysets", .{}); + defer rows.deinit(); + + while (rows.next()) |row| { + const keyset_info_encoded = row.blob(1); + // using arena allocator from result + const decoded_ks_info = try std.json.parseFromSliceLeaky(MintKeySetInfo, res.arena.allocator(), keyset_info_encoded, .{}); + + try res.value.append(decoded_ks_info); + } + + return res; + } + + pub fn addMintQuote(self: *Self, quote: MintQuote) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + const quote_json = try std.json.stringifyAlloc(self.allocator, quote, .{}); + defer self.allocator.free(quote_json); + + try conn.exec("INSERT INTO mint_quote (id, quote) VALUES (?1, ?2) ON CONFLICT DO UPDATE SET quote = ?2;", .{ + sqlite.blob("e.id.bin), + sqlite.blob(quote_json), + }); + } + + // caller must free MintQuote + pub fn getMintQuote(self: *Self, allocator: std.mem.Allocator, quote_id: zul.UUID) !?MintQuote { + { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + if (try conn.row("SELECT quote FROM mint_quote WHERE id = ?1", .{ + sqlite.blob("e_id.bin), + })) |row| { + defer row.deinit(); // must be called + const quote_json = row.blob(0); + + std.log.debug("quote-json: {s}", .{quote_json}); + + const quote = try std.json.parseFromSlice(MintQuote, self.allocator, quote_json, .{}); + defer quote.deinit(); + + return try quote.value.clone(allocator); + } + + return null; + } + } + + pub fn updateMintQuoteState( + self: *Self, + quote_id: zul.UUID, + state: nuts.nut04.QuoteState, + ) !nuts.nut04.QuoteState { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var quote = (try self.getMintQuote(self.allocator, quote_id)) orelse return error.UnknownQuote; + + const old_state = quote.state; + quote.state = state; + + const quote_json = try std.json.stringifyAlloc(self.allocator, quote, .{}); + defer self.allocator.free(quote_json); + + try conn.exec("UPDATE mint_quote SET quote = ?1 WHERE id = ?2", .{ + sqlite.blob(quote_json), + sqlite.blob("e_id.bin), + }); + + return old_state; + } + + /// caller must free array list and every elements + pub fn getMintQuotes(self: *Self, allocator: std.mem.Allocator) !std.ArrayList(MintQuote) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var rows = try conn.rows("SELECT quote FROM mint_quote", .{}); + defer rows.deinit(); + + var arena = std.heap.ArenaAllocator.init(self.allocator); + defer arena.deinit(); + + // creating result array list with caller allocator + var res = std.ArrayList(MintQuote).init(allocator); + errdefer { + for (res.items) |it| it.deinit(allocator); + res.deinit(); + } + + while (rows.next()) |row| { + const quote_json = row.blob(0); + + const quote = try std.json.parseFromSliceLeaky(MintQuote, arena.allocator(), quote_json, .{}); + + try res.append(try quote.clone(allocator)); + } + + return res; + } + + /// caller responsible to free resources + pub fn getMintQuoteByRequestLookupId( + self: *Self, + allocator: std.mem.Allocator, + request: []const u8, + ) !?MintQuote { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + // no need in free resources due arena + const quotes = try self.getMintQuotes(arena.allocator()); + for (quotes.items) |q| { + // if we found, cloning with allocator, so caller responsible on free resources + if (std.mem.eql(u8, q.request_lookup_id, request)) return try q.clone(allocator); + } + + return null; + } + /// caller responsible to free resources + pub fn getMintQuoteByRequest( + self: *Self, + allocator: std.mem.Allocator, + request: []const u8, + ) !?MintQuote { + const quotes = try self.getMintQuotes(self.allocator); + defer { + for (quotes.items) |q| q.deinit(self.allocator); + quotes.deinit(); + } + + for (quotes.items) |q| { + // if we found, cloning with allocator, so caller responsible on free resources + if (std.mem.eql(u8, q.request, request)) return try q.clone(allocator); + } + + return null; + } + + pub fn removeMintQuoteState( + self: *Self, + quote_id: zul.UUID, + ) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + try conn.exec("DELETE FROM mint_quote WHERE id = ?1", .{sqlite.blob("e_id.bin)}); + } + + pub fn addMeltQuote(self: *Self, quote: MeltQuote) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + const quote_json = try std.json.stringifyAlloc(self.allocator, quote, .{}); + defer self.allocator.free(quote_json); + + try conn.exec("INSERT INTO melt_quote (id, quote) VALUES (?1, ?2) ON CONFLICT DO UPDATE SET quote = ?2;", .{ + sqlite.blob("e.id.bin), + sqlite.blob(quote_json), + }); + } + + // caller must free MeltQuote + pub fn getMeltQuote(self: *Self, allocator: std.mem.Allocator, quote_id: zul.UUID) !?MeltQuote { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + if (try conn.row("SELECT quote FROM melt_quote WHERE id = ?1", .{ + sqlite.blob("e_id.bin), + })) |row| { + defer row.deinit(); // must be called + const quote_json = row.blob(0); + + const quote = try std.json.parseFromSlice(MeltQuote, self.allocator, quote_json, .{ .allocate = .alloc_always }); + defer quote.deinit(); + + return try quote.value.clone(allocator); + } + + return null; + } + + pub fn updateMeltQuoteState( + self: *Self, + quote_id: zul.UUID, + state: nuts.nut05.QuoteState, + ) !nuts.nut05.QuoteState { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var quote = (try self.getMeltQuote(self.allocator, quote_id)) orelse return error.UnknownQuote; + + const old_state = quote.state; + quote.state = state; + + const quote_json = try std.json.stringifyAlloc(self.allocator, quote, .{}); + defer self.allocator.free(quote_json); + + try conn.exec("UPDATE melt_quote SET quote = ?1 WHERE id = ?2", .{ + sqlite.blob(quote_json), + sqlite.blob("e_id.bin), + }); + + return old_state; + } + + /// caller must free array list and every elements + pub fn getMeltQuotes(self: *Self, allocator: std.mem.Allocator) !std.ArrayList(MeltQuote) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var rows = try conn.rows("SELECT quote FROM melt_quote", .{}); + defer rows.deinit(); + + var arena = std.heap.ArenaAllocator.init(self.allocator); + defer arena.deinit(); + + // creating result array list with caller allocator + var res = std.ArrayList(MeltQuote).init(allocator); + errdefer { + for (res.items) |it| it.deinit(allocator); + res.deinit(); + } + + while (rows.next()) |row| { + const quote_json = row.blob(0); + + const quote = try std.json.parseFromSliceLeaky(MeltQuote, arena.allocator(), quote_json, .{}); + + try res.append(try quote.clone(allocator)); + } + + return res; + } + + /// caller responsible to free resources + pub fn getMeltQuoteByRequestLookupId( + self: *Self, + allocator: std.mem.Allocator, + request: []const u8, + ) !?MeltQuote { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + // no need in free resources due arena + const quotes = try self.getMeltQuotes(arena.allocator()); + for (quotes.items) |q| { + // if we found, cloning with allocator, so caller responsible on free resources + if (std.mem.eql(u8, q.request_lookup_id, request)) return try q.clone(allocator); + } + + return null; + } + + /// caller responsible to free resources + pub fn getMeltQuoteByRequest( + self: *Self, + allocator: std.mem.Allocator, + request: []const u8, + ) !?MeltQuote { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + // no need in free resources due arena + const quotes = try self.getMeltQuotes(arena.allocator()); + for (quotes.items) |q| { + // if we found, cloning with allocator, so caller responsible on free resources + if (std.mem.eql(u8, q.request, request)) return try q.clone(allocator); + } + + return null; + } + + pub fn removeMeltQuoteState( + self: *Self, + quote_id: zul.UUID, + ) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + try conn.exec("DELETE FROM melt_quote WHERE id = ?1", .{sqlite.blob("e_id.bin)}); + } + + pub fn addProofs(self: *Self, proofs: []const nuts.Proof) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + try conn.transaction(); + errdefer conn.rollback(); + + for (proofs) |proof| { + // TODO fix witness encode + // const witness_json: ?[]const u8 = if (proof.witness) |w| try std.json.stringifyAlloc(self.allocator, w, .{}) else null; + const witness_json: ?[]const u8 = null; + defer if (witness_json) |wj| self.allocator.free(wj); + + std.log.debug("insert proof y {s}", .{ + (try proof.y()).toString(), + }); + + try conn.exec( + \\INSERT INTO proof + \\(y, amount, keyset_id, secret, c, witness, state) + \\VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7); + , .{ + sqlite.blob(&(try proof.y()).serialize()), + @as(i64, @intCast(proof.amount)), + &proof.keyset_id.toString(), + proof.secret.toBytes(), + sqlite.blob(&proof.c.pk.data), + witness_json, + "UNSPENT", + }); + } + + try conn.commit(); + } + + // caller must free resources + pub fn getProofsByYs(self: *Self, allocator: std.mem.Allocator, ys: []const secp256k1.PublicKey) !std.ArrayList(?nuts.Proof) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var ys_result = try std.ArrayList(?nuts.Proof).initCapacity(allocator, ys.len); + errdefer ys_result.deinit(); + errdefer for (ys_result.items) |y| if (y) |_y| _y.deinit(allocator); + + var arena = std.heap.ArenaAllocator.init(self.allocator); + defer arena.deinit(); + + for (ys) |y| { + if (try conn.row("SELECT * FROM proof WHERE y=?1;", .{sqlite.blob(&y.serialize())})) |row| { + defer row.deinit(); + + const proof = try sqliteRowToProof(arena.allocator(), row); + ys_result.appendAssumeCapacity(try proof.clone(allocator)); + } else ys_result.appendAssumeCapacity(null); + } + + return ys_result; + } + + // caller must deinit result std.ArrayList + pub fn updateProofsStates( + self: *Self, + allocator: std.mem.Allocator, + ys: []const secp256k1.PublicKey, + proofs_state: nuts.nut07.State, + ) !std.ArrayList(?nuts.nut07.State) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + try conn.transaction(); + errdefer conn.rollback(); + + var states = try std.ArrayList(?nuts.nut07.State).initCapacity(allocator, ys.len); + errdefer states.deinit(); + + const proofs_state_str = proofs_state.toString(); + + for (ys) |y| { + const y_bytes = y.serialize(); + + const currenct_state: ?nuts.nut07.State = if (try conn.row("SELECT state FROM proof WHERE y = ?1", .{sqlite.blob(&y_bytes)})) |row| v: { + defer row.deinit(); + break :v try nuts.nut07.State.fromString(row.text(0)); + } else null; + + states.appendAssumeCapacity(currenct_state); + + if (currenct_state) |cs| { + if (cs != .spent) { + try conn.exec("UPDATE proof SET state = ?1 WHERE y = ?2", .{ proofs_state_str, sqlite.blob(&y_bytes) }); + } + } + } + + try conn.commit(); + + return states; + } + + // caller must free result + pub fn getProofsStates(self: *Self, allocator: std.mem.Allocator, ys: []const secp256k1.PublicKey) !std.ArrayList(?nuts.nut07.State) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var states = try std.ArrayList(?nuts.nut07.State).initCapacity(allocator, ys.len); + errdefer states.deinit(); + + for (ys) |y| { + const row = try conn.row("SELECT * FROM proof WHERE y=?1;", .{ + sqlite.blob(&y.serialize()), + }); + + const state: ?nuts.nut07.State = if (row) |r| v: { + defer r.deinit(); + break :v try nuts.nut07.State.fromString(r.text(0)); + } else null; + + states.appendAssumeCapacity(state); + } + + return states; + } + + // result through Arena, for more easy deallocation + pub fn getProofsByKeysetId( + self: *Self, + allocator: std.mem.Allocator, + id: nuts.Id, + ) !Arened(std.meta.Tuple(&.{ + std.ArrayList(nuts.Proof), + std.ArrayList(?nuts.nut07.State), + })) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var rows = try conn.rows("SELECT amount, keyset_id, secret, c, witness, state FROM proof WHERE keyset_id=?1;", .{id.toString()}); + defer rows.deinit(); + + var res = try Arened(std.meta.Tuple(&.{ + std.ArrayList(nuts.Proof), + std.ArrayList(?nuts.nut07.State), + })).init(allocator); + errdefer res.deinit(); + + var proofs_for_id = std.ArrayList(nuts.Proof).init(res.arena.allocator()); + var states = std.ArrayList(?nuts.nut07.State).init(res.arena.allocator()); + + while (rows.next()) |r| { + const proof, const state = try sqliteRowToProofWithState(res.arena.allocator(), r); + try proofs_for_id.append(proof); + try states.append(state); + } + + res.value[0] = proofs_for_id; + res.value[1] = states; + + return res; + } + + pub fn addBlindSignatures( + self: *Self, + blinded_messages: []const secp256k1.PublicKey, + blind_signatures: []const nuts.BlindSignature, + quote_id: ?[]const u8, + ) !void { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + try conn.transaction(); + errdefer conn.rollback(); + + // zip to arrays + const max_len = @min(blinded_messages.len, blind_signatures.len); + + for (blinded_messages[0..max_len], blind_signatures[0..max_len]) |msg, signature| { + const sql = + \\ INSERT INTO blind_signature + \\ (y, amount, keyset_id, c, quote_id) + \\ VALUES (?1, ?2, ?3, ?4, ?5); + ; + + try conn.exec(sql, .{ + sqlite.blob(&msg.serialize()), + @as(i64, @intCast(signature.amount)), + signature.keyset_id.toString(), + sqlite.blob(&signature.c.serialize()), + quote_id, + }); + } + + try conn.commit(); + } + + pub fn getBlindSignatures( + self: *Self, + allocator: std.mem.Allocator, + blinded_messages: []const secp256k1.PublicKey, + ) !std.ArrayList(?nuts.BlindSignature) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var res = try std.ArrayList(?nuts.BlindSignature).initCapacity(allocator, blinded_messages.len); + errdefer res.deinit(); + + for (blinded_messages) |msg| { + const sql = + "SELECT amount, keyset_id, c FROM blind_signature WHERE y=?1;"; + + if (try conn.row(sql, .{sqlite.blob(&msg.serialize())})) |row| { + defer row.deinit(); + const blinded = try sqliteRowToBlindSignature(row); + + res.appendAssumeCapacity(blinded); + } else res.appendAssumeCapacity(null); + } + + return res; + } + + /// caller response to free resources + pub fn getBlindSignaturesForKeyset( + self: *Self, + allocator: std.mem.Allocator, + keyset_id: nuts.Id, + ) !std.ArrayList(nuts.BlindSignature) { + var conn = self.pool.acquire(); + defer self.pool.release(conn); + + var res = std.ArrayList(nuts.BlindSignature).init(allocator); + errdefer res.deinit(); + + const sql = + "SELECT amount, keyset_id, c FROM blind_signature WHERE keyset_id=?1;"; + + var rows = try conn.rows(sql, .{keyset_id.toString()}); + defer rows.deinit(); + + while (rows.next()) |row| { + const blinded = try sqliteRowToBlindSignature(row); + + try res.append(blinded); + } + + return res; + } +}; + +fn sqliteRowToBlindSignature(row: sqlite.Row) !nuts.BlindSignature { + const row_amount: i64 = row.int(0); + const keyset_id: []const u8 = row.text(1); + + const row_c = row.blob(2); + + return .{ + .amount = @abs(row_amount), + .keyset_id = try nuts.Id.fromStr(keyset_id), + .c = try secp256k1.PublicKey.fromSlice(row_c), + .dleq = null, + }; +} + +// amount, keyset_id, secret, c, witness +fn sqliteRowToProof(arena: std.mem.Allocator, row: sqlite.Row) !nuts.Proof { + const amount = row.int(0); + const keyset_id = row.text(1); + const row_secret = row.text(2); + const row_c = row.blob(3); + const wintess = row.nullableText(4); + + return .{ + .amount = @intCast(amount), + .keyset_id = try nuts.Id.fromStr(keyset_id), + .secret = .{ .inner = row_secret }, + .c = try secp256k1.PublicKey.fromSlice(row_c), + .witness = if (wintess) |w| try std.json.parseFromSliceLeaky(nuts.Witness, arena, w, .{}) else null, + .dleq = null, + }; +} + +// amount, keyset_id, secret, c, witness, state +fn sqliteRowToProofWithState(arena: std.mem.Allocator, row: sqlite.Row) !struct { nuts.Proof, ?nuts.State } { + const amount = row.int(0); + const keyset_id = row.text(1); + const row_secret = row.text(2); + const row_c = row.blob(3); + const wintess = row.nullableText(4); + + const row_state = row.nullableText(5); + + const state: ?nuts.nut07.State = if (row_state) |rs| try nuts.nut07.State.fromString(rs) else null; + + return .{ + .{ + .amount = @intCast(amount), + .keyset_id = try nuts.Id.fromStr(keyset_id), + .secret = .{ .inner = row_secret }, + .c = try secp256k1.PublicKey.fromSlice(row_c), + .witness = if (wintess) |w| try std.json.parseFromSliceLeaky(nuts.Witness, arena, w, .{}) else null, + .dleq = null, + }, + state, + }; +} diff --git a/src/core/database/wallet_memory.zig b/src/core/database/wallet_memory.zig index 1d65aa5..0b516fd 100644 --- a/src/core/database/wallet_memory.zig +++ b/src/core/database/wallet_memory.zig @@ -5,7 +5,9 @@ const MintKeySetInfo = @import("../mint/mint.zig").MintKeySetInfo; const MintQuote = @import("../mint/types.zig").MintQuote; // TODO import from wallet const MeltQuote = @import("../mint/types.zig").MeltQuote; // TODO import from wallet const ProofInfo = @import("../mint/types.zig").ProofInfo; -const secp256k1 = @import("secp256k1"); +const bitcoin_primitives = @import("bitcoin-primitives"); +const secp256k1 = bitcoin_primitives.secp256k1; +const zul = @import("zul"); /// TODO rw locks /// Wallet Memory Database @@ -14,11 +16,11 @@ pub const WalletMemoryDatabase = struct { lock: std.Thread.RwLock, - mints: std.AutoHashMap([]const u8, ?MintInfo), - mint_keysets: std.AutoHashMap([]const u8, std.AutoHashMap(nuts.Id, void)), + mints: std.StringHashMap(?MintInfo), + mint_keysets: std.StringHashMap(std.AutoHashMap(nuts.Id, void)), keysets: std.AutoHashMap(nuts.Id, nuts.KeySetInfo), - mint_quotes: std.AutoHashMap([16]u8, MintQuote), - melt_quotes: std.AutoHashMap([16]u8, MeltQuote), + mint_quotes: std.AutoHashMap(zul.UUID, MintQuote), + melt_quotes: std.AutoHashMap(zul.UUID, MeltQuote), mint_keys: std.AutoHashMap(nuts.Id, nuts.nut01.Keys), proofs: std.AutoHashMap(secp256k1.PublicKey, ProofInfo), keyset_counter: std.AutoHashMap(nuts.Id, u32), @@ -34,34 +36,39 @@ pub const WalletMemoryDatabase = struct { keyset_counter: std.AutoHashMap(nuts.Id, u32), nostr_last_checked: std.AutoHashMap(secp256k1.PublicKey, u32), ) !WalletMemoryDatabase { - var _mint_quotes = std.AutoHashMap([16]u8, MintQuote).init(allocator); + var _mint_quotes = std.AutoHashMap(zul.UUID, MintQuote).init(allocator); errdefer _mint_quotes.deinit(); for (mint_quotes) |q| { try _mint_quotes.put(q.id, q); } - var _melt_quotes = std.AutoHashMap([16]u8, MeltQuote).init(allocator); + var _melt_quotes = std.AutoHashMap(zul.UUID, MeltQuote).init(allocator); errdefer _melt_quotes.deinit(); for (melt_quotes) |q| { - try _melt_quotes.put(q.id, q); + try _melt_quotes.put(q.id, try q.clone(allocator)); } - var _mint_keys = std.AutoHashMap(nuts.Id, nuts.nut01.Keys); - errdefer _mint_keys.deinit(); + var _mint_keys = std.AutoHashMap(nuts.Id, nuts.nut01.Keys).init(allocator); + errdefer { + var it = _mint_keys.valueIterator(); + while (it.next()) |k| { + k.deinit(allocator); + } + } for (mint_keys) |k| { - try _mint_keys.put(nuts.Id.fromKeys(k), k); + try _mint_keys.put(try nuts.Id.fromKeys(allocator, k.inner), k); } - var mints = std.AutoHashMap([]u8, ?MintInfo).init(allocator); + var mints = std.StringHashMap(?MintInfo).init(allocator); errdefer mints.deinit(); var keysets = std.AutoHashMap(nuts.Id, nuts.KeySetInfo).init(allocator); errdefer keysets.deinit(); - var mint_keysets = std.AutoHashMap([]const u8, std.AutoHashMap(nuts.Id, void)).init(allocator); + var mint_keysets = std.StringHashMap(std.AutoHashMap(nuts.Id, void)).init(allocator); errdefer mint_keysets.deinit(); var proofs = std.AutoHashMap(secp256k1.PublicKey, ProofInfo).init(allocator); @@ -75,7 +82,7 @@ pub const WalletMemoryDatabase = struct { .keysets = keysets, .mint_quotes = _mint_quotes, .melt_quotes = _melt_quotes, - .mint_keys = mint_keys, + .mint_keys = _mint_keys, .proofs = proofs, .keyset_counter = keyset_counter, .nostr_last_checked = nostr_last_checked, @@ -90,7 +97,7 @@ pub const WalletMemoryDatabase = struct { self.lock.lock(); defer self.lock.unlock(); - try self.mints.put(mint_url, mint_info); + try self.mints.put(mint_url, mint_info.?); } pub fn removeMint( @@ -101,79 +108,154 @@ pub const WalletMemoryDatabase = struct { defer self.lock.unlock(); const kv = self.mints.fetchRemove(mint_url) orelse return; - kv.value.deinit(self.allocator); + kv.value.?.deinit(self.allocator); } - pub fn getMint(self: *Self, mint_url: []u8) !?MintInfo { + pub fn getMint(self: *Self, mint_url: []u8, allocator: std.mem.Allocator) !?MintInfo { self.lock.lockShared(); defer self.lock.unlockShared(); - return self.mints.get(mint_url); + const mint_info = self.mints.get(mint_url); + + if (mint_info == null) { + return null; + } + + return if (self.mints.get(mint_url)) |m| try m.?.clone(allocator) else null; } - pub fn getMints() !void { - // TODO + pub fn getMints(self: *Self, allocator: std.mem.Allocator) !std.StringHashMap(?MintInfo) { + self.lock.lockShared(); + defer self.lock.unlockShared(); + + var mints_copy = std.StringHashMap(?MintInfo).init(allocator); + + var it = self.mints.iterator(); + while (it.next()) |entry| { + const key_copy = try allocator.dupe(u8, entry.key_ptr.*); + try mints_copy.put(key_copy, entry.value_ptr.*); + } + + return mints_copy; } pub fn updateMintUrl( self: *Self, - mint_url: []u8, + old_mint_url: []u8, new_mint_url: []u8, ) !void { self.lock.lock(); defer self.lock.unlock(); - // TODO - const kv = self.mints.fetchRemove(mint_url) orelse return; - const new_value = kv.value; - kv.value.deinit(self.allocator); + const proofs = self.getProofs( + old_mint_url, + null, + null, + null, + self.allocator, + ) catch return error.CouldNotGetProofs; + + // Update proofs + var updated_proofs = std.ArrayList(ProofInfo).init(self.allocator); + defer updated_proofs.deinit(); + + var removed_ys = std.ArrayList(secp256k1.PublicKey).init(self.allocator); + defer removed_ys.deinit(); + + for (proofs.items) |proof| { + const new_proof = ProofInfo.init( + proof.proof, + new_mint_url, + proof.state, + proof.unit, + ); + try updated_proofs.append(new_proof); + } + + try self.updateProofs(updated_proofs.items, removed_ys.items); + + // Update mint quotes + const current_quotes = self.getMintQuotes(self.allocator) catch return error.CouldNotGetMintQuotes; + const quotes = current_quotes.items; + const time = unix_time(); - self.mints.put(new_mint_url, new_value); + for (quotes) |*quote| { + if (quote.expiry < time) { + quote.mint_url = new_mint_url; + } + try self.addMintQuote(quote.*); + } } pub fn addMintKeysets( self: *Self, mint_url: []u8, - keysets: std.ArrayList(MintKeySetInfo), + keysets: std.ArrayList(nuts.KeySetInfo), ) !void { self.lock.lock(); defer self.lock.unlock(); - var keyset_map = self.mint_keysets.get(mint_url); - if (keyset_map == null) { - keyset_map = try std.AutoHashMap(u64, void).init(self.allocator); - try self.mint_keysets.put(mint_url, keyset_map); + var keyset_ids = self.mint_keysets.get(mint_url); + + if (keyset_ids == null) { + const new_keyset_ids = std.AutoHashMap(nuts.Id, void).init(self.allocator); + keyset_ids = new_keyset_ids; } - for (keysets) |keyset_info| { - try keyset_map.put(keyset_info.id, {}); + var unwrapped_keyset_ids = keyset_ids.?; + + for (keysets.items) |keyset_info| { + try unwrapped_keyset_ids.put(keyset_info.id, {}); + } + + try self.mint_keysets.put(mint_url, unwrapped_keyset_ids); + + for (keysets.items) |keyset_info| { + try self.keysets.put(keyset_info.id, keyset_info); } } - pub fn getMintKeysets(self: *Self) !void { + pub fn getMintKeysets(self: *Self, allocator: std.mem.Allocator, mint_url: []u8) !?std.ArrayList(nuts.KeySetInfo) { self.lock.lockShared(); defer self.lock.unlockShared(); - // TODO + var keysets = std.ArrayList(nuts.KeySetInfo).init(allocator); + defer keysets.deinit(); + + const keyset_ids = self.mint_keysets.get(mint_url); + + var it = keyset_ids.?.iterator(); + while (it.next()) |kv| { + const id = kv.key_ptr.*; + if (self.keysets.get(id)) |keyset| { + try keysets.append(keyset); + } + } + + return keysets; } - pub fn getKeysetById(self: *Self) !void { + + pub fn getKeysetById(self: *Self, id: nuts.Id) !?nuts.KeySetInfo { self.lock.lockShared(); defer self.lock.unlockShared(); - // TODO + const keysets_id = self.keysets.get(id); + + if (keysets_id == null) { + return null; + } + return keysets_id.?; } pub fn addMintQuote(self: *Self, quote: MintQuote) !void { self.lock.lock(); defer self.lock.unlock(); - // TODO clone quote - try self.mint_quotes.put(quote.id, quote); } // caller must free MintQuote - pub fn getMintQuote(self: *Self, allocator: std.mem.Allocator, quote_id: [16]u8) !?MintQuote { + pub fn getMintQuote(self: *Self, allocator: std.mem.Allocator, quote_id: zul.UUID) !?MintQuote { self.lock.lockShared(); defer self.lock.unlockShared(); @@ -182,7 +264,7 @@ pub const WalletMemoryDatabase = struct { return try quote.clone(allocator); } - /// caller must free array list and every elements + // caller must free array list and every elements pub fn getMintQuotes(self: *Self, allocator: std.mem.Allocator) !std.ArrayList(MintQuote) { self.lock.lockShared(); defer self.lock.unlockShared(); @@ -202,11 +284,11 @@ pub const WalletMemoryDatabase = struct { return result; } - pub fn removeMintQuote(self: *Self, quote: MintQuote) !void { + pub fn removeMintQuote(self: *Self, quote_id: zul.UUID) !void { self.lock.lock(); defer self.lock.unlock(); - const kv = self.mint_quotes.fetchRemove(quote) orelse return; + const kv = self.mint_quotes.fetchRemove(quote_id) orelse return; kv.value.deinit(self.allocator); } @@ -217,7 +299,11 @@ pub const WalletMemoryDatabase = struct { try self.melt_quotes.put(quote.id, quote); } - pub fn getMeltQuote(self: *Self, allocator: std.mem.Allocator, quote_id: [16]u8) !?MeltQuote { + pub fn getMeltQuote( + self: *Self, + allocator: std.mem.Allocator, + quote_id: zul.UUID, + ) !?MeltQuote { self.lock.lockShared(); defer self.lock.unlockShared(); @@ -226,11 +312,11 @@ pub const WalletMemoryDatabase = struct { return try quote.clone(allocator); } - pub fn removeMeltQuote(self: *Self, quote: MeltQuote) !void { + pub fn removeMeltQuote(self: *Self, quote_id: zul.UUID) !void { self.lock.lock(); defer self.lock.unlock(); - const kv = self.melt_quotes.fetchRemove(quote) orelse return; + const kv = self.melt_quotes.fetchRemove(quote_id) orelse return; kv.value.deinit(self.allocator); } @@ -252,32 +338,92 @@ pub const WalletMemoryDatabase = struct { self.lock.lock(); defer self.lock.unlock(); - const kv = self.mint_keys.fetchRemove(id) orelse return; + var kv = self.mint_keys.fetchRemove(id) orelse return; kv.value.deinit(self.allocator); } - pub fn updateProofs() !void { - // TODO + pub fn updateProofs( + self: *Self, + added: []ProofInfo, + removed_ys: []secp256k1.PublicKey, + ) !void { + for (added) |proof_info| { + try self.proofs.put(proof_info.y, proof_info); + } + + for (removed_ys) |y| { + _ = self.proofs.remove(y); + } } - pub fn setPendingProofs() !void { - // TODO + pub fn setPendingProofs(self: *Self, proofs: []const secp256k1.PublicKey) !void { + for (proofs) |proof| { + if (self.proofs.get(proof)) |proof_info| { + var updated_proof_info = proof_info; + updated_proof_info.state = nuts.nut07.State.pending; + + try self.proofs.put(proof, updated_proof_info); + } + } } - pub fn reserveProofs() !void { - // TODO + pub fn reserveProofs(self: *Self, proofs: []const secp256k1.PublicKey) !void { + for (proofs) |proof| { + if (self.proofs.get(proof)) |proof_info| { + var updated_proof_info = proof_info; + updated_proof_info.state = nuts.nut07.State.reserved; + + try self.proofs.put(proof, updated_proof_info); + } + } } - pub fn setUnspentProofs() !void { - // TODO + pub fn setUnspentProofs(self: *Self, proofs: []const secp256k1.PublicKey) !void { + for (proofs) |proof| { + if (self.proofs.get(proof)) |proof_info| { + var updated_proof_info = proof_info; + updated_proof_info.state = nuts.nut07.State.unspent; + + try self.proofs.put(proof, updated_proof_info); + } + } } - pub fn getProofs() !void { - // TODO + pub fn getProofs( + self: *Self, + mint_url: ?[]u8, + unit: ?nuts.CurrencyUnit, + state: ?[]const nuts.nut07.State, + spending_conditions: ?[]const nuts.nut11.SpendingConditions, + allocator: std.mem.Allocator, + ) !std.ArrayList(ProofInfo) { + self.lock.lockShared(); + defer self.lock.unlockShared(); + + var result_list = std.ArrayList(ProofInfo).init(allocator); + + var it = self.proofs.iterator(); + + while (it.next()) |entry| { + var proof_info = entry.value_ptr.*; + if (proof_info.matchesConditions(mint_url.?, unit.?, state.?, spending_conditions.?)) { + try result_list.append(proof_info); + } + } + + return result_list; } - pub fn incrementKeysetCounter() !void { - // TODO + pub fn incrementKeysetCounter( + self: *Self, + id: nuts.Id, + count: u32, + ) !void { + self.lock.lock(); + defer self.lock.unlock(); + + const current_counter = self.keyset_counter.get(id) orelse 0; + return try self.keyset_counter.put(id, current_counter + count); } pub fn getKeysetCounter(self: *Self, id: nuts.Id) !?u32 { @@ -287,11 +433,30 @@ pub const WalletMemoryDatabase = struct { return self.keyset_counter.get(id); } - pub fn getNostrLastChecked() !void { - // TODO + pub fn getNostrLastChecked( + self: *Self, + verifying_key: secp256k1.PublicKey, + ) !?u32 { + self.lock.lockShared(); + defer self.lock.unlockShared(); + + return self.nostr_last_checked.get(verifying_key); } - pub fn addNostrLastChecked() !void { - // TODO + pub fn addNostrLastChecked( + self: *Self, + verifying_key: secp256k1.PublicKey, + last_checked: u32, + ) !void { + self.lock.lock(); + defer self.lock.unlock(); + + try self.nostr_last_checked.put(verifying_key, last_checked); } }; + +pub fn unix_time() u64 { + const timestamp = std.time.timestamp(); + const time: u64 = @intCast(@divFloor(timestamp, std.time.ns_per_s)); + return time; +} diff --git a/src/core/lib.zig b/src/core/lib.zig index 5749d71..0b51cd8 100644 --- a/src/core/lib.zig +++ b/src/core/lib.zig @@ -4,4 +4,5 @@ pub const amount = @import("amount.zig"); pub const nuts = @import("nuts/lib.zig"); pub const mint = @import("mint/mint.zig"); pub const mint_memory = @import("database/database.zig"); +pub const wallet_memory = @import("database/wallet_memory.zig"); pub const lightning = @import("lightning/lightning.zig"); diff --git a/src/core/lightning/mint.zig b/src/core/lightning/mint.zig index d5578da..7167ce5 100644 --- a/src/core/lightning/mint.zig +++ b/src/core/lightning/mint.zig @@ -21,7 +21,7 @@ const MintQuoteState = core.nuts.nut04.QuoteState; allocator: std.mem.Allocator, ptr: *anyopaque, -deinitFn: *const fn (ptr: *anyopaque) void, +deinitFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator) void, getSettingsFn: *const fn (ptr: *anyopaque) Settings, waitAnyInvoiceFn: *const fn (ptr: *anyopaque) ref.Arc(mpmc.UnboundedChannel(std.ArrayList(u8))), getPaymentQuoteFn: *const fn (ptr: *anyopaque, alloc: std.mem.Allocator, melt_quote_request: MeltQuoteBolt11Request) anyerror!PaymentQuoteResponse, @@ -61,11 +61,14 @@ pub fn initFrom(comptime T: type, allocator: std.mem.Allocator, value: T) !Self return self.createInvoice(arena, amount, unit, description, unix_expiry); } - pub fn deinit(pointer: *anyopaque) void { + pub fn deinit(pointer: *anyopaque, _allocator: std.mem.Allocator) void { + const self: *T = @ptrCast(@alignCast(pointer)); + if (std.meta.hasFn(T, "deinit")) { - const self: *T = @ptrCast(@alignCast(pointer)); self.deinit(); } + + _allocator.destroy(self); } }; @@ -76,6 +79,7 @@ pub fn initFrom(comptime T: type, allocator: std.mem.Allocator, value: T) !Self // ._type = T, .allocator = allocator, .ptr = ptr, + .getSettingsFn = gen.getSettings, .waitAnyInvoiceFn = gen.waitAnyInvoice, .getPaymentQuoteFn = gen.getPaymentQuote, @@ -86,10 +90,9 @@ pub fn initFrom(comptime T: type, allocator: std.mem.Allocator, value: T) !Self }; } -pub fn deinit(self: Self) void { - self.deinitFn(self.ptr); +pub fn deinit(self: Self, allocator: std.mem.Allocator) void { + self.deinitFn(self.ptr, allocator); // clearing pointer - // self.allocator.destroy(@as(self._type, @ptrCast(self.ptr))); } pub fn getSettings(self: Self) Settings { diff --git a/src/core/mint/lightning/lnbits.zig b/src/core/mint/lightning/lnbits.zig index cd4f0f0..4904afa 100644 --- a/src/core/mint/lightning/lnbits.zig +++ b/src/core/mint/lightning/lnbits.zig @@ -18,9 +18,7 @@ const FeeReserve = core.mint.FeeReserve; const Channel = @import("../../../channels/channels.zig").Channel; const MintLightning = core.lightning.MintLightning; -pub const HttpError = std.http.Client.RequestError || std.http.Client.Request.FinishError || std.http.Client.Request.WaitError || error{ ReadBodyError, WrongJson }; - -pub const LightningError = HttpError || std.Uri.ParseError || std.mem.Allocator.Error || error{ +pub const LightningError = error{ NotFound, Unauthorized, PaymentFailed, @@ -224,7 +222,8 @@ pub const LNBitsClient = struct { admin_key: []const u8, invoice_api_key: []const u8, lnbits_url: []const u8, - client: std.http.Client, + client: zul.http.Client, + _client: std.http.Client, pub fn init( allocator: std.mem.Allocator, @@ -232,9 +231,7 @@ pub const LNBitsClient = struct { invoice_api_key: []const u8, lnbits_url: []const u8, ) !LNBitsClient { - var client = std.http.Client{ - .allocator = allocator, - }; + var client = zul.http.Client.init(allocator); errdefer client.deinit(); return .{ @@ -242,11 +239,15 @@ pub const LNBitsClient = struct { .lnbits_url = lnbits_url, .invoice_api_key = invoice_api_key, .client = client, + ._client = std.http.Client{ + .allocator = allocator, + }, }; } pub fn deinit(self: *@This()) void { self.client.deinit(); + self._client.deinit(); } // get - request get, caller is owner of result slice (should deallocate it with allocator passed as argument) @@ -254,40 +255,46 @@ pub const LNBitsClient = struct { self: *@This(), allocator: std.mem.Allocator, endpoint: []const u8, - ) LightningError![]const u8 { + ) !std.ArrayList(u8) { const uri = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ self.lnbits_url, endpoint }); defer allocator.free(uri); - const header_buf = try allocator.alloc(u8, 1024 * 1024 * 4); - defer allocator.free(header_buf); + var response = std.ArrayList(u8).init(allocator); + errdefer response.deinit(); + + const resp = std.http.Client.fetch(&self._client, .{ + .location = .{ + .url = uri, + }, + .max_append_size = 10 * 1024 * 1024, - var req = try self.client.open(.GET, try std.Uri.parse(uri), .{ - .server_header_buffer = header_buf, .extra_headers = &.{ .{ .name = "X-Api-Key", .value = self.admin_key, }, }, - }); - defer req.deinit(); - - try req.send(); - try req.finish(); - try req.wait(); + .method = .GET, + .response_storage = .{ + .dynamic = &response, + }, + }) catch |err| { + switch (err) { + // skip + error.EndOfStream => { + std.log.debug("endofstream body len({d}) = {s}", .{ response.items.len, response.items }); + return err; + }, + else => return err, + } + }; - if (req.response.status != .ok) { - if (req.response.status == .not_found) return LightningError.NotFound; + if (resp.status != .ok) { + if (resp.status == .not_found) return LightningError.NotFound; + if (resp.status == .unauthorized) return LightningError.Unauthorized; } - var rdr = req.reader(); - const body = rdr.readAllAlloc(allocator, 1024 * 1024 * 4) catch |err| { - std.log.debug("read body error: {any}", .{err}); - return error.ReadBodyError; - }; - errdefer allocator.free(body); - - return body; + return response; } pub fn post( @@ -295,51 +302,50 @@ pub const LNBitsClient = struct { allocator: std.mem.Allocator, endpoint: []const u8, req_body: []const u8, - ) LightningError![]const u8 { + ) !std.ArrayList(u8) { const uri = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ self.lnbits_url, endpoint }); defer allocator.free(uri); - const header_buf = try allocator.alloc(u8, 1024 * 1024 * 4); - defer allocator.free(header_buf); + var response = std.ArrayList(u8).init(allocator); + errdefer response.deinit(); - std.log.debug("uri: {s}", .{uri}); - - var req = try self.client.open(.POST, try std.Uri.parse(uri), .{ - .server_header_buffer = header_buf, + const resp = std.http.Client.fetch(&self._client, .{ + .location = .{ + .url = uri, + }, .extra_headers = &.{ .{ .name = "X-Api-Key", .value = self.admin_key, }, - .{ .name = "accept", .value = "*/*", }, }, - }); - defer req.deinit(); - - req.transfer_encoding = .{ .content_length = req_body.len }; - - try req.send(); - try req.writeAll(req_body); - try req.finish(); - try req.wait(); + .method = .POST, + .payload = req_body, + .max_append_size = 10 * 1024 * 1024, + .response_storage = .{ + .dynamic = &response, + }, + }) catch |err| { + switch (err) { + // skip + error.EndOfStream => { + std.log.debug("endofstream body len({d}) = {s}", .{ response.items.len, response.items }); + return err; + }, + else => return err, + } + }; - if (req.response.status != .ok) { - if (req.response.status == .not_found) return LightningError.NotFound; - if (req.response.status == .unauthorized) return LightningError.Unauthorized; + if (resp.status != .ok) { + if (resp.status == .not_found) return LightningError.NotFound; + if (resp.status == .unauthorized) return LightningError.Unauthorized; } - var rdr = req.reader(); - const body = rdr.readAllAlloc(allocator, 1024 * 1024 * 4) catch |err| { - std.log.debug("read post body error: {any}", .{err}); - return error.ReadBodyError; - }; - errdefer allocator.free(body); - - return body; + return response; } /// createInvoice - creating invoice @@ -349,33 +355,20 @@ pub const LNBitsClient = struct { .emit_null_optional_fields = false, }); - std.log.debug("request {s}", .{req_body}); - const res = try self.post(allocator, "api/v1/payments", req_body); + defer res.deinit(); - std.log.debug("create invoice, response : {s}", .{res}); - - const parsed = std.json.parseFromSlice(std.json.Value, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; - - const payment_request = parsed.value.object.get("payment_request") orelse unreachable; - const payment_hash = parsed.value.object.get("payment_hash") orelse unreachable; - - const pr = switch (payment_request) { - .string => |v| try allocator.dupe(u8, v), - else => unreachable, - }; - errdefer allocator.free(pr); + const parsed = std.json.parseFromSlice(CreateInvoiceResponse, allocator, res.items, .{ + .allocate = .alloc_always, + .ignore_unknown_fields = true, + }) catch |err| { + errdefer std.log.debug("Parse create invoice error: {any}, body:\n{s}", .{ err, res.items }); - const ph = switch (payment_hash) { - .string => |v| try allocator.dupe(u8, v), - else => unreachable, + return error.WrongJson; }; - errdefer allocator.free(ph); + defer parsed.deinit(); - return .{ - .payment_hash = ph, - .payment_request = pr, - }; + return try parsed.value.clone(allocator); } /// payInvoice - paying invoice @@ -384,8 +377,9 @@ pub const LNBitsClient = struct { const req_body = try std.json.stringifyAlloc(allocator, &.{ .out = true, .bolt11 = bolt11 }, .{}); const res = try self.post(allocator, "api/v1/payments", req_body); + defer res.deinit(); - const parsed = std.json.parseFromSlice(std.json.Value, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; + const parsed = std.json.parseFromSlice(std.json.Value, allocator, res.items, .{ .allocate = .alloc_always }) catch return error.WrongJson; const payment_hash = parsed.value.object.get("payment_hash") orelse unreachable; @@ -422,7 +416,9 @@ pub const LNBitsClient = struct { defer allocator.free(endpoint); const res = try self.get(allocator, endpoint); - const parsed = std.json.parseFromSlice(std.json.Value, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; + defer res.deinit(); + + const parsed = std.json.parseFromSlice(std.json.Value, allocator, res.items, .{ .allocate = .alloc_always }) catch return error.WrongJson; const is_paid = parsed.value.object.get("paid") orelse unreachable; @@ -446,8 +442,9 @@ pub const LNBitsClient = struct { defer allocator.free(endpoint); const res = try self.get(allocator, endpoint); + errdefer std.log.debug("cannot decode find invoice {s}", .{res.items}); - return std.json.parseFromSlice([]const FindInvoiceResponse, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; + return std.json.parseFromSlice([]const FindInvoiceResponse, allocator, res.items, .{ .allocate = .alloc_always }) catch return error.WrongJson; } /// Create invoice webhook @@ -561,6 +558,15 @@ pub const CreateInvoiceResponse = struct { /// Payment request (bolt11) payment_request: []const u8, + pub fn clone(self: CreateInvoiceResponse, alloc: std.mem.Allocator) !CreateInvoiceResponse { + var s: CreateInvoiceResponse = undefined; + errdefer s.deinit(alloc); + + s.payment_hash = try alloc.dupe(u8, self.payment_hash); + s.payment_request = try alloc.dupe(u8, self.payment_request); + return s; + } + pub fn deinit(self: CreateInvoiceResponse, alloc: std.mem.Allocator) void { alloc.free(self.payment_hash); alloc.free(self.payment_request); diff --git a/src/core/mint/mint.zig b/src/core/mint/mint.zig index e1d864f..d6d6544 100644 --- a/src/core/mint/mint.zig +++ b/src/core/mint/mint.zig @@ -241,7 +241,7 @@ pub const Mint = struct { // not contains in array const derivation_path = derivationPathFromUnit(unit, 0); - const keyset, const keyset_info = try createNewKeysetAlloc( + var keyset, const keyset_info = try createNewKeysetAlloc( allocator, secp_ctx, xpriv, @@ -252,6 +252,7 @@ pub const Mint = struct { fee, ); defer keyset_info.deinit(allocator); + errdefer keyset.deinit(); const id = keyset_info.id; _ = try localstore.addKeysetInfo(keyset_info); @@ -690,7 +691,7 @@ pub const Mint = struct { try self.localstore .value - .addBlindSignatures(blinded_messages.items, blind_signatures.items); + .addBlindSignatures(blinded_messages.items, blind_signatures.items, &mint_request.quote); _ = try self.localstore.value.updateMintQuoteState(quote_id, .issued); @@ -935,6 +936,7 @@ pub const Mint = struct { .addBlindSignatures( blinded_messages.items, promises.items, + null, ); return .{ @@ -1094,6 +1096,7 @@ pub const Mint = struct { /// In the event that a melt request fails and the lighthing payment is not made /// The [`Proofs`] should be returned to an unspent state and the quote should be unpaid pub fn processUnpaidMelt(self: *Mint, melt_request: core.nuts.nut05.MeltBolt11Request) !void { + std.log.debug("processing unpaid melt", .{}); var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); @@ -1204,6 +1207,7 @@ pub const Mint = struct { .addBlindSignatures( blinded_messages.items, change_sigs.items, + melt_request.quote, ); change = change_sigs; diff --git a/src/core/mint/types.zig b/src/core/mint/types.zig index 93c438c..0d4497c 100644 --- a/src/core/mint/types.zig +++ b/src/core/mint/types.zig @@ -4,7 +4,10 @@ const amount_lib = @import("../lib.zig").amount; const CurrencyUnit = @import("../lib.zig").nuts.CurrencyUnit; const MintQuoteState = @import("../lib.zig").nuts.nut04.QuoteState; const MeltQuoteState = @import("../lib.zig").nuts.nut05.QuoteState; -const secp256k1 = @import("secp256k1"); +const Nut10Secret = @import("../lib.zig").nuts.nut10.Secret; +const secret_lib = @import("../secret.zig"); +const bitcoin_primitives = @import("bitcoin-primitives"); +const secp256k1 = bitcoin_primitives.secp256k1; const zul = @import("zul"); /// Mint Quote Info @@ -179,6 +182,8 @@ pub const MeltQuote = struct { }; pub const ProofInfo = struct { + const Self = @This(); + /// Proof proof: nuts.Proof, /// y @@ -199,14 +204,129 @@ pub const ProofInfo = struct { state: nuts.nut07.State, unit: nuts.CurrencyUnit, ) ProofInfo { - const secret = nuts.nut10.Secret.fromSecret(proof.secret); + const parsed_secret = Nut10Secret.fromSecret(proof.secret, std.heap.page_allocator) catch null; + const secret = parsed_secret.?.value; + return .{ .proof = proof, .y = proof.c, .mint_url = mint_url, .state = state, - .spending_conditions = nuts.nut10.toSpendingConditions(secret) catch null, + .spending_condition = Nut10Secret.toSpendingConditions(secret, std.heap.page_allocator) catch null, .unit = unit, }; } + + pub fn matchesConditions( + self: *Self, + mint_url: ?[]u8, + currency_unit: ?nuts.CurrencyUnit, + state: ?[]const nuts.nut07.State, + spending_conditions: ?[]const nuts.nut11.SpendingConditions, + ) bool { + if (mint_url) |url| { + if (std.mem.eql(u8, url, self.mint_url) == false) { + return false; + } + } + + if (currency_unit) |unit| { + if (unit == self.unit) { + return false; + } + } + + if (state) |s| { + if (!containsState(s, self.state)) { + return false; + } + } + + if (spending_conditions) |conds| { + if (self.spending_condition) |spending_condition| { + switch (spending_condition) { + else => { + if (!containsCondition(conds, spending_condition)) { + return false; + } + }, + } + } else { + return false; + } + } + + return true; + } + + fn containsState(states: []const nuts.nut07.State, state: nuts.nut07.State) bool { + for (states) |s| { + if (s == state) { + return true; + } + } + return false; + } + + fn containsCondition(conditions: []const nuts.nut11.SpendingConditions, cond: nuts.nut11.SpendingConditions) bool { + for (conditions) |c| { + if (compareSpendingConditions(c, cond) == true) { + return true; + } + } + return false; + } + + pub fn compareSpendingConditions(a: nuts.nut11.SpendingConditions, b: nuts.nut11.SpendingConditions) bool { + if (compareTag(a, b) == false) { + return false; + } + + switch (a) { + nuts.nut11.SpendingConditions.p2pk => |a_p2pk| { + const b_p2pk = b.p2pk; + if (!secp256k1.PublicKey.eql(a_p2pk.data, b_p2pk.data)) { + return false; + } + if (!compareConditions(a_p2pk.conditions, b_p2pk.conditions)) { + return false; + } + }, + nuts.nut11.SpendingConditions.htlc => |a_htlc| { + const b_htlc = b.htlc; + if (!std.mem.eql(u8, &a_htlc.data, &b_htlc.data)) { + return false; + } + if (!compareConditions(a_htlc.conditions, b_htlc.conditions)) { + return false; + } + }, + } + + return true; + } + + fn compareTag(a: nuts.nut11.SpendingConditions, b: nuts.nut11.SpendingConditions) bool { + return switch (a) { + nuts.nut11.SpendingConditions.p2pk => switch (b) { + nuts.nut11.SpendingConditions.p2pk => true, + else => false, + }, + nuts.nut11.SpendingConditions.htlc => switch (b) { + nuts.nut11.SpendingConditions.htlc => true, + else => false, + }, + }; + } + + fn compareConditions(a: ?nuts.nut11.Conditions, b: ?nuts.nut11.Conditions) bool { + if (a == null and b == null) { + return true; + } + if (a == null or b == null) { + return false; + } + + return true; + } }; diff --git a/src/core/nuts/nut00/nut00.zig b/src/core/nuts/nut00/nut00.zig index 0e1ec3b..f028a2e 100644 --- a/src/core/nuts/nut00/nut00.zig +++ b/src/core/nuts/nut00/nut00.zig @@ -84,6 +84,13 @@ pub const Witness = union(enum) { return undefined; } + pub fn jsonStringify(self: *const Witness, out: anytype) !void { + switch (self.*) { + inline .htlc_witness => |w| try out.print("{s}", .{std.json.fmt(w, .{})}), + inline .p2pk_witness => |w| try out.print("{s}", .{std.json.fmt(w, .{})}), + } + } + pub fn jsonParse(allocator: std.mem.Allocator, _source: anytype, options: std.json.ParseOptions) !@This() { const parsed = try std.json.innerParse(std.json.Value, allocator, _source, options); diff --git a/src/core/nuts/nut01/nut01.zig b/src/core/nuts/nut01/nut01.zig index be80717..0a86e09 100644 --- a/src/core/nuts/nut01/nut01.zig +++ b/src/core/nuts/nut01/nut01.zig @@ -45,6 +45,61 @@ pub const KeysResponse = struct { .keysets = try arraylist.toOwnedSlice(), }; } + + // we expect allocator is arena + pub fn sort(self: KeysResponse, allocator: std.mem.Allocator) !KeysResponse { + var sorted_keysets = std.ArrayList(KeySet).init(allocator); + + for (self.keysets) |keyset| { + const keys = keyset.keys; + + var key_array = std.ArrayList([]const u8).init(allocator); + defer key_array.deinit(); + + var it = keys.inner.iterator(); + while (it.next()) |kv| { + const key = kv.key_ptr.*; + try key_array.append(key); + } + + const Context = struct { + items: [][]const u8, + }; + var context = Context{ .items = key_array.items }; + + std.sort.insertion( + []const u8, + key_array.items, + &context, + struct { + fn lessThan(_: *Context, a: []const u8, b: []const u8) bool { + const keyA = std.fmt.parseInt(u64, a, 10) catch 0; + const keyB = std.fmt.parseInt(u64, b, 10) catch 0; + return keyA < keyB; + } + }.lessThan, + ); + + var sorted_keys = std.StringHashMap(secp256k1.PublicKey).init(allocator); + + for (key_array.items) |key| { + const value = keys.inner.get(key).?; + + try sorted_keys.put(try allocator.dupe(u8, key), value); + } + + const sorted_keyset = KeySet{ + .id = keyset.id, + .unit = keyset.unit, + .keys = Keys{ .inner = sorted_keys }, + }; + + try sorted_keysets.append(sorted_keyset); + } + + const sorted_pubkeys = KeysResponse{ .keysets = try sorted_keysets.toOwnedSlice() }; + return sorted_pubkeys; + } }; /// Mint Keys [NUT-01] diff --git a/src/core/nuts/nut02/nut02.zig b/src/core/nuts/nut02/nut02.zig index 20ee518..99ff85c 100644 --- a/src/core/nuts/nut02/nut02.zig +++ b/src/core/nuts/nut02/nut02.zig @@ -13,7 +13,7 @@ const MintKeys = @import("../nut01/nut01.zig").MintKeys; const MintKeyPair = @import("../nut01/nut01.zig").MintKeyPair; /// Keyset version -pub const KeySetVersion = enum { +pub const KeySetVersion = enum(u8) { /// Current Version 00 version00, @@ -44,6 +44,10 @@ pub const Id = struct { version: KeySetVersion, id: [BYTELEN]u8, + pub fn toString(self: Id) [STRLEN + 2]u8 { + return ("00" ++ std.fmt.bytesToHex(self.id, .lower)).*; + } + pub fn toBytes(self: Id) [BYTELEN + 1]u8 { return [_]u8{self.version.toByte()} ++ self.id; } diff --git a/src/core/nuts/nut04/nut04.zig b/src/core/nuts/nut04/nut04.zig index b0a5d87..9f30628 100644 --- a/src/core/nuts/nut04/nut04.zig +++ b/src/core/nuts/nut04/nut04.zig @@ -44,6 +44,12 @@ pub const QuoteState = enum { pub fn jsonStringify(self: *const QuoteState, out: anytype) !void { try out.write(self.toStr()); } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !QuoteState { + const state = try std.json.innerParse([]const u8, allocator, source, options); + + return QuoteState.fromStr(state) catch error.UnexpectedToken; + } }; pub const MintMethodSettings = struct { diff --git a/src/core/nuts/nut05/nut05.zig b/src/core/nuts/nut05/nut05.zig index debfcd1..b102fb0 100644 --- a/src/core/nuts/nut05/nut05.zig +++ b/src/core/nuts/nut05/nut05.zig @@ -55,12 +55,12 @@ pub const QuoteState = enum { try out.write(self.toString()); } - pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, _: std.json.ParseOptions) !QuoteState { - const state = try std.json.innerParse([]const u8, allocator, source, .{}); + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !QuoteState { + const state = try std.json.innerParse([]const u8, allocator, source, options); return QuoteState.fromString(state) catch { std.log.debug("wrong state value: {s}", .{state}); - return error.UnexpectedError; + return error.UnexpectedToken; }; } }; diff --git a/src/core/nuts/nut06/nut06.zig b/src/core/nuts/nut06/nut06.zig index 03b436c..ea25442 100644 --- a/src/core/nuts/nut06/nut06.zig +++ b/src/core/nuts/nut06/nut06.zig @@ -124,6 +124,34 @@ pub const MintInfo = struct { mint_icon_url: ?[]const u8 = null, /// message of the day that the wallet must display to the user motd: ?[]const u8 = null, + + pub fn deinit(self: MintInfo, allocator: std.mem.Allocator) void { + allocator.free(self.name.?); + // TODO check all fields that were allocated + } + + pub fn clone(self: MintInfo, allocator: std.mem.Allocator) !MintInfo { + var cloned = self; + + const name = try allocator.dupe(u8, self.name.?); + errdefer allocator.free(name); + + const description = try allocator.dupe(u8, self.description.?); + errdefer allocator.free(description); + + const description_long = try allocator.dupe(u8, self.description_long.?); + errdefer allocator.free(description_long); + + const motd = try allocator.dupe(u8, self.motd.?); + errdefer allocator.free(motd); + + cloned.name = name; + cloned.description = description; + cloned.description_long = description_long; + cloned.motd = motd; + + return cloned; + } }; /// Check state Settings diff --git a/src/core/nuts/nut10/nut10.zig b/src/core/nuts/nut10/nut10.zig index 8ce2229..306d0c0 100644 --- a/src/core/nuts/nut10/nut10.zig +++ b/src/core/nuts/nut10/nut10.zig @@ -137,16 +137,16 @@ pub const Secret = struct { }; } - pub fn toSpendingConditions(self: Secret, allocator: std.mem.Allocator) !SpendingConditions { - switch (self.kind) { + pub fn toSpendingConditions(self: ?Secret, allocator: std.mem.Allocator) !SpendingConditions { + switch (self.?.kind) { .p2pk => { - if (self.secret_data.data.len != 33) { + if (self.?.secret_data.data.len != 33) { return error.InvalidPublicKeyLength; } - const pubkey = try secp256k1.PublicKey.fromSlice(self.secret_data.data); + const pubkey = try secp256k1.PublicKey.fromSlice(self.?.secret_data.data); // Parse optional conditions from `tags` - const conditions = if (self.secret_data.tags) |tags| + const conditions = if (self.?.secret_data.tags) |tags| try Conditions.fromTags(tags, allocator) else null; @@ -160,14 +160,14 @@ pub const Secret = struct { }; }, .htlc => { - if (self.secret_data.data.len != 32) { + if (self.?.secret_data.data.len != 32) { return error.InvalidHashLength; } var hash: [32]u8 = undefined; - @memcpy(&hash, self.secret_data.data); + @memcpy(&hash, self.?.secret_data.data); // Parse optional conditions from `tags` - const conditions = if (self.secret_data.tags) |tags| + const conditions = if (self.?.secret_data.tags) |tags| try Conditions.fromTags(tags, allocator) else null; diff --git a/src/lightning_invoices/builder.zig b/src/lightning_invoices/builder.zig index 904de9d..0ff8dee 100644 --- a/src/lightning_invoices/builder.zig +++ b/src/lightning_invoices/builder.zig @@ -180,15 +180,18 @@ pub fn setMinFinalCltvExpiryDelta(self: *InvoiceBuilder, delta: u64) !void { /// Sets the payment secret and relevant features. pub fn setPaymentSecret(self: *InvoiceBuilder, gpa: std.mem.Allocator, payment_secret: PaymentSecret) !void { + _ = gpa; // autofix self.secret_flag = true; var found_features = false; for (self.tagged_fields.items) |*f| { switch (f.*) { .features => |*field| { + _ = field; // autofix found_features = true; - try field.set(Features.tlv_onion_payload_required); - try field.set(Features.payment_addr_required); + // TODO set after + // try field.set(Features.tlv_onion_payload_required); + // try field.set(Features.payment_addr_required); }, else => continue, } @@ -197,14 +200,16 @@ pub fn setPaymentSecret(self: *InvoiceBuilder, gpa: std.mem.Allocator, payment_s self.tagged_fields.appendAssumeCapacity(.{ .payment_secret = payment_secret }); if (!found_features) { - var features = Features{ - .flags = std.AutoHashMap(Features.FeatureBit, void).init(gpa), - }; + // TODO implement features + // var features = Features{ + // .flags = std.AutoHashMap(Features.FeatureBit, void).init(gpa), + // ._flags = undefined, + // }; - try features.set(Features.tlv_onion_payload_required); - try features.set(Features.payment_addr_required); + // try features.set(Features.tlv_onion_payload_required); + // try features.set(Features.payment_addr_required); - self.tagged_fields.appendAssumeCapacity(.{ .features = features }); + // self.tagged_fields.appendAssumeCapacity(.{ .features = features }); } } diff --git a/src/lightning_invoices/features.zig b/src/lightning_invoices/features.zig index 8b42307..fb731b2 100644 --- a/src/lightning_invoices/features.zig +++ b/src/lightning_invoices/features.zig @@ -69,7 +69,10 @@ //! [BOLT #9]: https://github.com/lightning/bolts/blob/master/09-features.md const std = @import("std"); const bech32 = @import("bitcoin-primitives").bech32; +const ser = @import("ser.zig"); +const invoice = @import("invoice.zig"); +const Writer = ser.Writer; pub const FeatureBit = u16; const Features = @This(); @@ -336,37 +339,106 @@ pub inline fn isFeatureRequired(b: FeatureBit) bool { /// /// This is not exported to bindings users as we map the concrete feature types below directly instead /// Note that, for convenience, flags is LITTLE endian (despite being big-endian on the wire) -flags: std.AutoHashMap(FeatureBit, void), +flags: std.ArrayList(u8), -pub fn set(self: *Features, b: FeatureBit) !void { - try self.flags.put(b, {}); -} +// pub fn set(self: *Features, b: FeatureBit) !void { +// try self.flags.put(b, {}); +// } pub fn deinit(self: *Features) void { self.flags.deinit(); } +pub fn base32Len(self: *const Features) usize { + // its hack (use allocator from features) + var writer = std.ArrayList(u5).init(self.flags.allocator); + defer writer.deinit(); + + var w = Writer.init(&writer); + + // TODO: rewrite to fix em + self.writeBase32(&w) catch unreachable; + + return writer.items.len; +} + +pub fn writeBase32(self: *const Features, writer: *const Writer) !void { + // Explanation for the "4": the normal way to round up when dividing is to add the divisor + // minus one before dividing + const length_u5s: usize = (self.flags.items.len * 8 + 4) / 5; + + var res_u5s = try std.ArrayList(u5).initCapacity(self.flags.allocator, length_u5s); + defer res_u5s.deinit(); + + res_u5s.appendNTimesAssumeCapacity(0, length_u5s); + + for (self.flags.items, 0..) |byte, byte_idx| { + const bit_pos_from_left_0_indexed = byte_idx * 8; + const new_u5_idx = length_u5s - @as(usize, @intCast(bit_pos_from_left_0_indexed / 5)) - 1; + const new_bit_pos = bit_pos_from_left_0_indexed % 5; + const shifted_chunk_u16 = std.math.shl(u16, byte, new_bit_pos); + const curr_u5_as_u8: u8 = @intCast(res_u5s.items[new_u5_idx]); + res_u5s.items[new_u5_idx] = + @intCast(curr_u5_as_u8 | @as(u8, @intCast((shifted_chunk_u16 & 0x001f)))); + + if (new_u5_idx > 0) { + const _curr_u5_as_u8: u8 = res_u5s.items[new_u5_idx - 1]; + res_u5s.items[new_u5_idx - 1] = @intCast(_curr_u5_as_u8 | @as(u8, @intCast((shifted_chunk_u16 >> 5) & 0x001f))); + } + + if (new_u5_idx > 1) { + const _curr_u5_as_u8: u8 = res_u5s.items[new_u5_idx - 2]; + res_u5s.items[new_u5_idx - 2] = + @intCast(_curr_u5_as_u8 | @as(u8, @intCast(((shifted_chunk_u16 >> 10) & 0x001f)))); + } + } + + // Trim the highest feature bits. + while (res_u5s.items.len != 0 and res_u5s.items[0] == 0) { + _ = res_u5s.orderedRemove(0); + } + + try writer.write(res_u5s.items); +} + pub fn fromBase32(gpa: std.mem.Allocator, data: []const u5) !Features { - const field_data = try bech32.arrayListFromBase32(gpa, data); - defer field_data.deinit(); + // Explanation for the "7": the normal way to round up when dividing is to add the divisor + // minus one before dividing + const length_bytes = (data.len * 5 + 7) / 8; + + var res_bytes = try std.ArrayList(u8).initCapacity(gpa, length_bytes); + errdefer res_bytes.deinit(); - const width: usize = 5; + res_bytes.appendNTimesAssumeCapacity(0, length_bytes); - var flags = std.AutoHashMap(FeatureBit, void).init(gpa); - errdefer flags.deinit(); + for (data, 0..) |chunk, u5_idx| { + const bit_pos_from_right_0_indexed = (data.len - u5_idx - 1) * 5; + const new_byte_idx = (bit_pos_from_right_0_indexed / 8); + const new_bit_pos = bit_pos_from_right_0_indexed % 8; + const chunk_u16 = @as(u16, chunk); - // Set feature bits from parsed data. - const bits_number = data.len * width; - for (0..bits_number) |i| { - const byte_index = i / width; - const bit_index = i % width; + res_bytes.items[new_byte_idx] |= @intCast(std.math.shl(u16, chunk_u16, new_bit_pos) & 0xff); - if ((std.math.shl(u8, data[data.len - byte_index - 1], bit_index)) & 1 == 1) { - try flags.put(@truncate(i), {}); + if (new_byte_idx != length_bytes - 1) { + res_bytes.items[new_byte_idx + 1] |= @intCast((std.math.shr(u16, chunk_u16, 8 - new_bit_pos)) & 0xff); } } + // Trim the highest feature bits. + while (res_bytes.items.len != 0 and res_bytes.items[res_bytes.items.len - 1] == 0) { + _ = res_bytes.pop(); + } + return .{ - .flags = flags, + .flags = res_bytes, }; } + +test "encode/decode" { + + // { 0, 65, 2, 2 }, data { 1, 0, 4, 16, 8, 0 } + var f = try Features.fromBase32(std.testing.allocator, &.{ 1, 0, 4, 16, 8, 0 }); + defer f.deinit(); + + try std.testing.expectEqualSlices(u8, f.flags.items, &.{ 0, 65, 2, 2 }); +} diff --git a/src/lightning_invoices/invoice.zig b/src/lightning_invoices/invoice.zig index cfe6caa..bb43f38 100644 --- a/src/lightning_invoices/invoice.zig +++ b/src/lightning_invoices/invoice.zig @@ -729,7 +729,7 @@ pub const RawTaggedField = union(enum) { } }; -fn calculateBase32Len(size: usize) usize { +pub inline fn calculateBase32Len(size: usize) usize { const bits = size * 8; return if (bits % 5 == 0) @@ -1020,10 +1020,9 @@ pub const TaggedField = union(enum) { .payment_metadata => |pm| { try write_tagged_field(writer, constants.TAG_PAYMENT_METADATA, pm.items); }, - // TODO implement other - // features: Features, - - else => {}, + .features => |f| { + try write_tagged_field(writer, constants.TAG_FEATURES, f); + }, } } @@ -1299,6 +1298,20 @@ test "full serialize" { } } +test "ln invoice" { + const str = + \\lnbc550n1pn04xe4sp53aqjsrd2wg58e7ve4erj8kklssaqg929uzzsdc6tzxg04jcvpa3qpp59sd92rxj89he4uzqg2d4xjcr3lvwa2pfhq3e2xxcwny6wkm024fqdpqf38xy6t5wvszs3z9f48jq5692fty253fxqrpcgcqpjrzjqdm9ng9v36em3598yqg5alyxr5afgquzmnapgqm5dd8c76ew3qgt5rpgrgqq3hcqqqqqqqlgqqqqqqqqvs9qxpqysgqjumakl745mg5djjxvtjz5n3upkz4gtsedd0vyf3a359crdcwvm7rlzkvpe87tnhphjjfp8mly79j0wz4hrrst68mfaz96lhj385q2dgpr42g8l + ; + + var inv = try Bolt11Invoice.fromStr(std.testing.allocator, str); + defer inv.deinit(); + + const str_2 = try inv.signed_invoice.toStrAlloc(std.testing.allocator); + defer std.testing.allocator.free(str_2); + + try std.testing.expectEqualStrings(str, str_2); +} + test { _ = @import("builder.zig"); } diff --git a/src/mint.zig b/src/mint.zig index c745bee..8aadbdb 100644 --- a/src/mint.zig +++ b/src/mint.zig @@ -77,8 +77,12 @@ pub fn main() !void { }; defer clap_res.deinit(); + var arena = std.heap.ArenaAllocator.init(gpa.allocator()); + defer arena.deinit(); + const config_path = clap_res.args.config orelse "config.toml"; + // TODO add work dir var parsed_settings = try config.Settings.initFromToml(gpa.allocator(), config_path); defer parsed_settings.deinit(); @@ -98,6 +102,19 @@ pub fn main() !void { break :v try MintDatabase.initFrom(MintMemoryDatabase, gpa.allocator(), db); }, + inline .sqlite => v: { + // TODO custom path to database? + + const path = try arena.allocator().dupeZ(u8, parsed_settings.value.sqlite.?.path); + + var db = try core.mint_memory.MintSqliteDatabase.initFrom( + gpa.allocator(), + path.ptr, + ); + errdefer db.deinit(); + + break :v try MintDatabase.initFrom(core.mint_memory.MintSqliteDatabase, gpa.allocator(), db); + }, else => { // not implemented engine unreachable; @@ -171,14 +188,11 @@ pub fn main() !void { var it = ln_backends.valueIterator(); while (it.next()) |v| { - v.deinit(); + v.deinit(gpa.allocator()); } ln_backends.deinit(); } - var arena = std.heap.ArenaAllocator.init(gpa.allocator()); - defer arena.deinit(); - // TODO set ln router // additional routers for httpz server switch (parsed_settings.value.ln.ln_backend) { @@ -188,7 +202,7 @@ pub fn main() !void { const invoice_api_key = lnbits_settings.invoice_api_key; const webhook_endpoint = "/webhook/lnbits/sat/invoice"; - var webhook_url = zul.StringBuilder.init(gpa.allocator()); + var webhook_url = zul.StringBuilder.init(arena.allocator()); try webhook_url .write(mint_url); @@ -201,7 +215,7 @@ pub fn main() !void { break :val try ref.Arc(mpmc.UnboundedChannel(std.ArrayList(u8))).init(gpa.allocator(), ch); }; - errdefer chan.releaseWithFn((struct { + defer chan.releaseWithFn((struct { fn deinit(self: mpmc.UnboundedChannel(std.ArrayList(u8))) void { self.deinit(); } @@ -222,7 +236,7 @@ pub fn main() !void { errdefer lnbits.deinit(); const ln_mint = try lnbits.toMintLightning(gpa.allocator()); - errdefer ln_mint.deinit(); + errdefer ln_mint.deinit(gpa.allocator()); const unit = core.nuts.CurrencyUnit.sat; @@ -235,7 +249,7 @@ pub fn main() !void { const webhook_router = try lnbits.client.createInvoiceWebhookRouter( arena.allocator(), webhook_endpoint, - chan.retain(), + chan, ); try global_handler.router.append(webhook_router); @@ -250,7 +264,7 @@ pub fn main() !void { errdefer wallet.deinit(); const ln_mint = try wallet.toMintLightning(gpa.allocator()); - errdefer ln_mint.deinit(); + errdefer ln_mint.deinit(gpa.allocator()); try ln_backends.put(ln_key, ln_mint); @@ -331,7 +345,14 @@ pub fn main() !void { 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); + var mint = try Mint.init( + gpa.allocator(), + parsed_settings.value.info.url, + &try mnemonic.toSeedNormalized(&.{}), + mint_info, + localstore, + supported_units, + ); defer mint.deinit(); // Check the status of any mint quotes that are pending @@ -363,13 +384,10 @@ pub fn main() !void { // add lnn router here to server try handleInterrupt(&srv); + var wg = std.Thread.WaitGroup{}; // Spawn task to wait for invoces to be paid and update mint quotes // handle invoices - const threads = v: { - var threads = try std.ArrayList(std.Thread).initCapacity(gpa.allocator(), ln_backends.count()); - errdefer threads.deinit(); - errdefer for (threads.items) |t| t.detach(); - + const channels: std.ArrayList(ref.Arc(mpmc.UnboundedChannel(std.ArrayList(u8)))) = v: { const thread_fn = (struct { fn handleLnInvoice(m: *Mint, wait_ch: ref.Arc(mpmc.UnboundedChannel(std.ArrayList(u8)))) void { defer wait_ch.releaseWithFn((struct { @@ -381,7 +399,8 @@ pub fn main() !void { var receiver = wait_ch.value.receiver() catch return; while (true) { - var request_lookup_id = receiver.recv() orelse unreachable; + // receive lookup id, otherwise channel is closed (application is terminated) + var request_lookup_id = receiver.recv() orelse return; defer request_lookup_id.releaseWithFn((struct { fn deinit(_self: std.ArrayList(u8)) void { _self.deinit(); @@ -397,22 +416,38 @@ pub fn main() !void { }).handleLnInvoice; var it = ln_backends.iterator(); + + var channels = try std.ArrayList(ref.Arc(mpmc.UnboundedChannel(std.ArrayList(u8)))).initCapacity(gpa.allocator(), ln_backends.count()); + errdefer channels.deinit(); + while (it.next()) |ln_entry| { - threads.appendAssumeCapacity(try std.Thread.spawn(.{}, thread_fn, .{ - &mint, ln_entry.value_ptr.waitAnyInvoice(), - })); + // hack to stop channels + const ch = + ln_entry.value_ptr.waitAnyInvoice(); + channels.appendAssumeCapacity(ch); + + wg.spawnManager(thread_fn, .{ + &mint, + ch, + }); } - break :v threads; + + break :v channels; }; - defer threads.deinit(); - defer for (threads.items) |t| t.detach(); + defer channels.deinit(); 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", .{}); + // hack to stop channels (and threads with handle invoice) + for (channels.items) |ch| { + ch.value.close(); + } + + std.log.warn("Stopped server", .{}); + // defer wg.wait(); } pub fn handleInterrupt(srv: *httpz.Server(*http_router.GlobalRouter)) !void { diff --git a/src/mintd/config.example.toml b/src/mintd/config.example.toml index 023eeed..dea6579 100644 --- a/src/mintd/config.example.toml +++ b/src/mintd/config.example.toml @@ -24,6 +24,9 @@ mnemonic = "" # Database engine (sqlite/redb) defaults to sqlite # engine = "sqlite" +[sqlite] +# path = "" default ./cocomint_sqlite.db + [ln] # Required ln backend `cln`, `lnd`, `strike`, `fakewallet`, 'lnbits', 'phoenixd' ln_backend = "lnbits" diff --git a/src/mintd/config.zig b/src/mintd/config.zig index 7b1bd6f..e2a69ed 100644 --- a/src/mintd/config.zig +++ b/src/mintd/config.zig @@ -14,6 +14,7 @@ pub const Settings = struct { lnbits: ?Lnbits, fake_wallet: ?FakeWallet, database: Database, + sqlite: ?Sqlite = null, pub fn initFromToml(gpa: std.mem.Allocator, config_file_name: []const u8) !zig_toml.Parsed(Settings) { var parser = zig_toml.Parser(Settings).init(gpa); @@ -25,6 +26,10 @@ pub const Settings = struct { } }; +pub const Sqlite = struct { + path: []const u8 = "./cocomint_db.sqlite", +}; + pub const DatabaseEngine = enum { sqlite, redb, diff --git a/src/misc/http_router/http_router.zig b/src/misc/http_router/http_router.zig index 596e3ef..c83792f 100644 --- a/src/misc/http_router/http_router.zig +++ b/src/misc/http_router/http_router.zig @@ -90,7 +90,7 @@ test "ttt" { pub usingnamespace DefaultDispatcher(@This()); - pub fn testik(self: @This(), req: *httpz.Request, res: *httpz.Response) !void { + pub fn test_func(self: @This(), req: *httpz.Request, res: *httpz.Response) !void { _ = req; // autofix res.body = self.s; } @@ -106,7 +106,7 @@ test "ttt" { defer arena.deinit(); var some_router = try SomeHandlerRouter.init(arena.allocator(), SomeHandler.dispatcher, sh); - some_router.get("/test14", SomeHandler.testik, .{}); + some_router.get("/test14", SomeHandler.test_func, .{}); const ht = @import("httpz").testing; var router = GlobalRouter{ diff --git a/src/router/router_handlers.zig b/src/router/router_handlers.zig index fdfae7e..d972532 100644 --- a/src/router/router_handlers.zig +++ b/src/router/router_handlers.zig @@ -1,20 +1,29 @@ const std = @import("std"); + +const bitcoin_primitives = @import("bitcoin-primitives"); const httpz = @import("httpz"); -const core = @import("../core/lib.zig"); +const secp256k1 = bitcoin_primitives.secp256k1; const zul = @import("zul"); -const ln_invoice = @import("../lightning_invoices/invoice.zig"); +const core = @import("../core/lib.zig"); +const ln_invoice = @import("../lightning_invoices/invoice.zig"); const MintLightning = core.lightning.MintLightning; const MintState = @import("router.zig").MintState; const LnKey = @import("router.zig").LnKey; +const KeySet = @import("../core/nuts/nut02/nut02.zig").KeySet; +const KeysResponse = @import("../core/nuts/nut01/nut01.zig").KeysResponse; +const Keys = @import("../core/nuts/nut01/nut01.zig").Keys; pub fn getKeys(state: MintState, req: *httpz.Request, res: *httpz.Response) !void { const pubkeys = try state.mint.pubkeys(req.arena); + const pubkeys_sorted = try pubkeys.sort(req.arena); - return try res.json(pubkeys, .{}); + return try res.json(pubkeys_sorted, .{}); } pub fn getKeysets(state: MintState, req: *httpz.Request, res: *httpz.Response) !void { + errdefer std.log.debug("{any}", .{@errorReturnTrace()}); + const keysets = try state.mint.getKeysets(req.arena); return try res.json(keysets, .{}); @@ -36,6 +45,8 @@ pub fn getCheckMintBolt11Quote( req: *httpz.Request, res: *httpz.Response, ) !void { + errdefer std.log.debug("{any}", .{@errorReturnTrace()}); + const quote_id_hex = req.param("quote_id") orelse return error.ExpectQuoteId; const quote_id = try zul.UUID.parse(quote_id_hex); @@ -92,6 +103,8 @@ pub fn getMintBolt11Quote( req: *httpz.Request, res: *httpz.Response, ) !void { + errdefer std.log.debug("{any}", .{@errorReturnTrace()}); + std.log.debug("get mint bolt11 quote req", .{}); const payload = (try req.json(core.nuts.nut04.MintQuoteBolt11Request)) orelse return error.WrongRequest;