Skip to content

Commit

Permalink
nut04 mint resp, some mint methods + types
Browse files Browse the repository at this point in the history
  • Loading branch information
StringNick committed Sep 6, 2024
1 parent 8005dff commit 79f04ce
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 8 deletions.
76 changes: 73 additions & 3 deletions src/core/mint/mint.zig
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
const std = @import("std");
const core = @import("../lib.zig");
const MintInfo = core.nuts.MintInfo;
const secp256k1 = @import("secp256k1");
const bip32 = @import("bitcoin").bitcoin.bip32;
const helper = @import("../../helper/helper.zig");
const nuts = core.nuts;

const RWMutex = helper.RWMutex;
const MintInfo = core.nuts.MintInfo;
const MintQuoteBolt11Response = core.nuts.nut04.MintQuoteBolt11Response;
const MintQuoteState = core.nuts.nut04.QuoteState;

pub const MintQuote = @import("types.zig").MintQuote;
pub const MeltQuote = @import("types.zig").MeltQuote;
pub const MintMemoryDatabase = core.mint_memory.MintMemoryDatabase;

/// Mint Fee Reserve
pub const FeeReserve = struct {
Expand Down Expand Up @@ -61,9 +69,71 @@ pub const Mint = struct {
/// Mint Info
mint_info: MintInfo,
/// Mint Storage backend
// pub localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
localstore: helper.RWMutex(MintMemoryDatabase),
/// Active Mint Keysets
// keysets: Arc<RwLock<HashMap<Id, MintKeySet>>>,
keysets: RWMutex(std.AutoHashMap(nuts.Id, nuts.MintKeySet)),
secp_ctx: secp256k1.Secp256k1,
xpriv: bip32.ExtendedPrivKey,

/// Creating new [`MintQuote`], all arguments are cloned and reallocated
/// caller responsible on free resources of result
pub fn newMintQuote(
self: *const Mint,
allocator: std.mem.Allocator,
mint_url: []const u8,
request: []const u8,
unit: nuts.CurrencyUnit,
amount: core.amount.Amount,
expiry: u64,
ln_lookup: []const u8,
) !MintQuote {
const nut04 = self.mint_info.nuts.nut04;
if (nut04.disabled) return error.MintingDisabled;
if (nut04.getSettings(unit, .bolt11)) |settings| {
if (settings.max_amount) |max_amount| if (amount > max_amount) return error.MintOverLimit;

if (settings.min_amount) |min_amount| if (amount < min_amount) return error.MintUnderLimit;
} else return error.UnsupportedUnit;

const quote = try MintQuote.init(allocator, mint_url, request, unit, amount, expiry, ln_lookup.clone());
errdefer quote.deinit(allocator);

std.log.debug("New mint quote: {any}", .{quote});

self.localstore.lock.lock();

defer self.localstore.lock.unlock();
try self.localstore.value.addMintQuote(quote);

return quote;
}

/// Check mint quote
/// caller own result and should deinit
pub fn checkMintQuote(self: *const Mint, allocator: std.mem.Allocator, quote_id: [16]u8) !MintQuoteBolt11Response {
const quote = v: {
self.localstore.lock.lockShared();
defer self.localstore.lock.unlockShared();
break :v (try self.localstore.value.getMintQuote(allocator, quote_id)) orelse return error.UnknownQuote;
};
defer quote.deinit(allocator);

const paid = quote.state == .paid;

// Since the pending state is not part of the NUT it should not be part of the response.
// In practice the wallet should not be checking the state of a quote while waiting for the mint response.
const state = switch (quote.state) {
.pending => MintQuoteState.paid,
else => quote.state,
};

const result = MintQuoteBolt11Response{
.quote = quote.id,
.request = quote.request,
.paid = paid,
.state = state,
.expiry = quote.expiry,
};
return try result.clone(allocator);
}
};
18 changes: 13 additions & 5 deletions src/core/mint/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ pub const MintQuote = struct {

/// Create new [`MintQuote`]
pub fn init(
mint_url: std.Uri,
allocator: std.mem.Allocator,
mint_url: []const u8,
request: []const u8,
unit: CurrencyUnit,
amount: amount_lib.Amount,
expiry: u64,
request_lookup_id: []const u8,
) MintQuote {
) !MintQuote {
const id = zul.UUID.v4();

return .{
const mint_quote: MintQuote = .{
.mint_url = mint_url,
.id = id.bin,
.amount = amount,
Expand All @@ -46,6 +47,8 @@ pub const MintQuote = struct {
.expiry = expiry,
.request_lookup_id = request_lookup_id,
};

return try mint_quote.clone(allocator);
}

pub fn deinit(self: *const MintQuote, allocator: std.mem.Allocator) void {
Expand All @@ -54,15 +57,20 @@ pub const MintQuote = struct {
}

pub fn clone(self: *const MintQuote, allocator: std.mem.Allocator) !MintQuote {
const request_lookup = try allocator.alloc(u8, self.request_lookup_id.len);
const request_lookup = try allocator.dupe(u8, self.request_lookup_id);
errdefer allocator.free(request_lookup);

const request = try allocator.alloc(u8, self.request.len);
const request = try allocator.dupe(u8, self.request);
errdefer allocator.free(request);

const mint_url = try allocator.dupe(u8, self.mint_url);
errdefer allocator.free(mint_url);

var cloned = self.*;

cloned.request = request;
cloned.request_lookup_id = request_lookup;
cloned.mint_url = mint_url;

return cloned;
}
Expand Down
196 changes: 196 additions & 0 deletions src/core/nuts/nut04/nut04.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const std = @import("std");
const CurrencyUnit = @import("../nut00/nut00.zig").CurrencyUnit;
const Proof = @import("../nut00/nut00.zig").Proof;
const PaymentMethod = @import("../nut00/nut00.zig").PaymentMethod;
const MintQuote = @import("../../mint/types.zig").MintQuote;

pub const QuoteState = enum {
/// Quote has not been paid
Expand All @@ -16,6 +17,15 @@ pub const QuoteState = enum {
pending,
/// ecash issued for quote
issued,

pub fn fromStr(s: []const u8) !QuoteState {
if (std.mem.eql(u8, "UNPAID", s)) return .unpaid;
if (std.mem.eql(u8, "PAID", s)) return .paid;
if (std.mem.eql(u8, "PENDING", s)) return .pending;
if (std.mem.eql(u8, "ISSUED", s)) return .issued;

return error.UnknownState;
}
};

pub const MintMethodSettings = struct {
Expand All @@ -35,4 +45,190 @@ pub const Settings = struct {
methods: []const MintMethodSettings = &.{},
/// Minting disabled
disabled: bool = false,

/// Get [`MintMethodSettings`] for unit method pair
pub fn getSettings(
self: Settings,
unit: CurrencyUnit,
method: PaymentMethod,
) ?MintMethodSettings {
for (self.methods) |method_settings| {
if (method_settings.method == method and method_settings.unit == unit) return method_settings;
}

return null;
}
};

/// Mint quote response [NUT-04]
pub const MintQuoteBolt11Response = struct {
/// Quote Id
quote: []const u8,
/// Payment request to fulfil
request: []const u8,
// TODO: To be deprecated
/// Whether the the request haas be paid
/// Deprecated
paid: ?bool,
/// Quote State
state: QuoteState,
/// Unix timestamp until the quote is valid
expiry: ?u64,

pub fn clone(self: *const @This(), allocator: std.mem.Allocator) !@This() {
const quote = try allocator.dupe(u8, self.quote);
errdefer allocator.free(quote);

const request = try allocator.dupe(u8, self.request);
errdefer allocator.free(request);

var cloned = self.*;
cloned.request = request;
cloned.quote = quote;
return cloned;
}

pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void {
allocator.free(self.request);
allocator.free(self.request);
}

/// Without reallocating slices, so lifetime of result as [`MintQuote`]
pub fn fromMintQuote(mint_quote: MintQuote) !MintQuoteBolt11Response {
const paid = mint_quote.state == .paid;
return .{
.quote = mint_quote.id,
.request = mint_quote.request,
.paid = paid,
.state = mint_quote.state,
.expiry = mint_quote.expiry,
};
}

pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !MintQuoteBolt11Response {
const value = std.json.innerParse(std.json.Value, allocator, source, .{ .allocate = .alloc_always }) catch return error.UnexpectedToken;

if (value != .object) return error.UnexpectedToken;

const quote: []const u8 = try std.json.parseFromValueLeaky(
[]const u8,
allocator,
value.object.get("quote") orelse return error.UnexpectedToken,
options,
);

const request: []const u8 = try std.json.parseFromValueLeaky(
[]const u8,
allocator,
value.object.get("request") orelse return error.UnexpectedToken,
options,
);

const paid: ?bool = v: {
break :v try std.json.parseFromValueLeaky(
bool,
allocator,
value.object.get("paid") orelse break :v null,
options,
);
};

const state: ?[]const u8 = v: {
break :v try std.json.parseFromValueLeaky(
[]const u8,
allocator,
value.object.get("state") orelse break :v null,
options,
);
};
const expiry: ?u64 = v: {
break :v try std.json.parseFromValueLeaky(
[]u64,
allocator,
value.object.get("expiry") orelse break :v null,
options,
);
};

const _state: QuoteState = if (state) |s|
// wrong quote state
QuoteState.fromStr(s) catch error.UnexpectedToken
else if (paid) |p|
if (p) .paid else .unpaid
else
return error.UnexpectedError;

return .{
.state = _state,
.expiry = expiry,
.request = request,
.quote = quote,
.paid = paid,
};
}
};

// A custom deserializer is needed until all mints
// update some will return without the required state.
// impl<'de> Deserialize<'de> for MintQuoteBolt11Response {
// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
// where
// D: Deserializer<'de>,
// {
// let value = Value::deserialize(deserializer)?;

// let quote: String = serde_json::from_value(
// value
// .get("quote")
// .ok_or(serde::de::Error::missing_field("quote"))?
// .clone(),
// )
// .map_err(|_| serde::de::Error::custom("Invalid quote id string"))?;

// let request: String = serde_json::from_value(
// value
// .get("request")
// .ok_or(serde::de::Error::missing_field("request"))?
// .clone(),
// )
// .map_err(|_| serde::de::Error::custom("Invalid request string"))?;

// let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());

// let state: Option<String> = value
// .get("state")
// .and_then(|s| serde_json::from_value(s.clone()).ok());

// let (state, paid) = match (state, paid) {
// (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
// (Some(state), _) => {
// let state: QuoteState = QuoteState::from_str(&state)
// .map_err(|_| serde::de::Error::custom("Unknown state"))?;
// let paid = state == QuoteState::Paid;

// (state, paid)
// }
// (None, Some(paid)) => {
// let state = if paid {
// QuoteState::Paid
// } else {
// QuoteState::Unpaid
// };
// (state, paid)
// }
// };

// let expiry = value
// .get("expiry")
// .ok_or(serde::de::Error::missing_field("expiry"))?
// .as_u64();

// Ok(Self {
// quote,
// request,
// paid: Some(paid),
// state,
// expiry,
// })
// }
// }
8 changes: 8 additions & 0 deletions src/helper/helper.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
const std = @import("std");

// TODO add atomic ref count?
pub fn RWMutex(comptime T: type) type {
return struct {
value: T,
lock: std.Thread.RwLock,
};
}

pub inline fn copySlice(allocator: std.mem.Allocator, slice: []const u8) ![]u8 {
const allocated = try allocator.alloc(u8, slice.len);

Expand Down

0 comments on commit 79f04ce

Please sign in to comment.