From 3bdd0e88553c6869986abf976d3cc513b6396b80 Mon Sep 17 00:00:00 2001 From: lanaivina <31368580+lana-shanghai@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:27:54 +0200 Subject: [PATCH] Feat/wallet memory methods (#21) * WIP on feat/wallet_memory_methods * WIP on feat/wallet_memory_methods * address comments, add clone on MintInfo --- src/core/database/wallet_memory.zig | 291 ++++++++++++++++++++++------ src/core/lib.zig | 1 + src/core/mint/types.zig | 126 +++++++++++- src/core/nuts/nut06/nut06.zig | 28 +++ src/core/nuts/nut10/nut10.zig | 16 +- 5 files changed, 388 insertions(+), 74 deletions(-) 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/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/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;