Skip to content

Commit 5cadccd

Browse files
authored
one test for builder + split to sep file (#20)
1 parent 979bb30 commit 5cadccd

File tree

2 files changed

+288
-201
lines changed

2 files changed

+288
-201
lines changed

src/lightning_invoices/builder.zig

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
const std = @import("std");
2+
const bitcoin_primitives = @import("bitcoin-primitives");
3+
const bip32 = bitcoin_primitives.bips.bip32;
4+
const secp256k1 = bitcoin_primitives.secp256k1;
5+
const invoice_lib = @import("invoice.zig");
6+
7+
const Currency = invoice_lib.Currency;
8+
const SiPrefix = invoice_lib.SiPrefix;
9+
const TaggedField = invoice_lib.TaggedField;
10+
const RawTaggedField = invoice_lib.RawTaggedField;
11+
const PaymentSecret = invoice_lib.PaymentSecret;
12+
const Features = @import("features.zig");
13+
const Bolt11Invoice = invoice_lib.Bolt11Invoice;
14+
const RawDataPart = invoice_lib.RawDataPart;
15+
const RawBolt11Invoice = invoice_lib.RawBolt11Invoice;
16+
const RawHrp = invoice_lib.RawHrp;
17+
const Sha256 = std.crypto.hash.sha2.Sha256;
18+
const Message = secp256k1.Message;
19+
const RecoverableSignature = secp256k1.ecdsa.RecoverableSignature;
20+
21+
const InvoiceBuilder = @This();
22+
23+
currency: Currency,
24+
amount: ?u64,
25+
si_prefix: ?SiPrefix,
26+
timestamp: ?u64,
27+
tagged_fields: std.ArrayList(TaggedField),
28+
29+
/// exactly one [`TaggedField.description`] or [`TaggedField.description_hash`]
30+
description_flag: bool = false,
31+
32+
/// exactly one [`TaggedField.payment_hash`]
33+
hash_flag: bool = false,
34+
35+
/// the timestamp is set
36+
timestamp_flag: bool = false,
37+
38+
/// the CLTV expiry is set
39+
cltv_flag: bool = false,
40+
41+
/// the payment secret is set
42+
secret_flag: bool = false,
43+
44+
/// payment metadata is set
45+
payment_metadata_flag: bool = false,
46+
47+
/// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before
48+
/// `InvoiceBuilder.build(self)` becomes available.
49+
pub fn init(allocator: std.mem.Allocator, currency: Currency) !InvoiceBuilder {
50+
return .{
51+
.currency = currency,
52+
.amount = null,
53+
.si_prefix = null,
54+
.timestamp = null,
55+
.tagged_fields = try std.ArrayList(TaggedField).initCapacity(allocator, 8),
56+
};
57+
}
58+
59+
pub fn deinit(self: InvoiceBuilder) void {
60+
self.tagged_fields.deinit();
61+
}
62+
63+
/// Builds a [`RawBolt11Invoice`] if no [`CreationError`] occurred while construction any of the
64+
/// fields.
65+
pub fn buildRaw(self: *const InvoiceBuilder, gpa: std.mem.Allocator) !RawBolt11Invoice {
66+
const hrp = RawHrp{
67+
.currency = self.currency,
68+
.raw_amount = self.amount,
69+
.si_prefix = self.si_prefix,
70+
};
71+
72+
const timestamp = self.timestamp orelse @panic("expected timestamp");
73+
74+
var tagged_fields = try std.ArrayList(RawTaggedField).initCapacity(gpa, self.tagged_fields.items.len);
75+
errdefer tagged_fields.deinit();
76+
77+
// we moving ownership of TaggedField
78+
for (self.tagged_fields.items) |tf| {
79+
tagged_fields.appendAssumeCapacity(.{ .known = tf });
80+
}
81+
82+
const data = RawDataPart{
83+
.timestamp = timestamp,
84+
.tagged_fields = tagged_fields,
85+
};
86+
87+
return .{
88+
.hrp = hrp,
89+
.data = data,
90+
};
91+
}
92+
93+
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY fail with
94+
/// an error of type `E` and MUST produce a recoverable signature valid for the given hash and
95+
/// if applicable also for the included payee public key.
96+
pub fn tryBuildSigned(self: *const InvoiceBuilder, gpa: std.mem.Allocator, sign_function: *const fn (Message) anyerror!RecoverableSignature) !Bolt11Invoice {
97+
var raw = try self.buildRaw(gpa);
98+
errdefer raw.deinit();
99+
100+
const invoice = Bolt11Invoice{
101+
.signed_invoice = try raw.sign(gpa, sign_function),
102+
};
103+
104+
// TODO
105+
//invoice.check_field_counts().expect("should be ensured by type signature of builder");
106+
// invoice.check_feature_bits().expect("should be ensured by type signature of builder");
107+
// invoice.check_amount().expect("should be ensured by type signature of builder");
108+
109+
return invoice;
110+
}
111+
112+
/// Set the description. This function is only available if no description (hash) was set.
113+
/// Copy description
114+
pub fn setDescription(self: *InvoiceBuilder, gpa: std.mem.Allocator, description: []const u8) !void {
115+
if (self.description_flag) return error.DescriptionAlreadySet;
116+
117+
const _description = try gpa.dupe(
118+
u8,
119+
description,
120+
);
121+
errdefer gpa.free(_description);
122+
123+
self.description_flag = true;
124+
125+
self.tagged_fields.appendAssumeCapacity(.{
126+
.description = .{
127+
.inner = std.ArrayList(u8).fromOwnedSlice(gpa, _description),
128+
},
129+
});
130+
}
131+
132+
/// Set the description hash. This function is only available if no description (hash) was set.
133+
pub fn setDescriptionHash(self: *InvoiceBuilder, description_hash: [Sha256.digest_length]u8) !void {
134+
if (self.description_flag) return error.DescriptionAlreadySet;
135+
self.description_flag = true;
136+
137+
self.tagged_fields.appendAssumeCapacity(.{
138+
.description_hash = .{
139+
.inner = description_hash,
140+
},
141+
});
142+
}
143+
144+
/// Set the payment hash. This function is only available if no payment hash was set.
145+
pub fn setPaymentHash(self: *InvoiceBuilder, hash: [Sha256.digest_length]u8) !void {
146+
if (self.hash_flag) return error.PaymentHashAlreadySet;
147+
148+
self.hash_flag = true;
149+
150+
self.tagged_fields.appendAssumeCapacity(.{
151+
.payment_hash = .{
152+
.inner = hash,
153+
},
154+
});
155+
}
156+
157+
/// Sets the timestamp to a specific .
158+
pub fn setTimestamp(self: *InvoiceBuilder, time: u64) !void {
159+
if (self.timestamp_flag) return error.TimestampAlreadySet;
160+
161+
self.timestamp_flag = true;
162+
163+
self.timestamp = time;
164+
}
165+
/// Sets the timestamp to a specific .
166+
pub fn setCurrentTimestamp(self: *InvoiceBuilder) !void {
167+
if (self.timestamp_flag) return error.TimestampAlreadySet;
168+
169+
self.timestamp_flag = true;
170+
171+
self.timestamp = @intCast(std.time.timestamp());
172+
}
173+
174+
/// Sets `min_final_cltv_expiry_delta`.
175+
pub fn setMinFinalCltvExpiryDelta(self: *InvoiceBuilder, delta: u64) !void {
176+
if (self.cltv_flag) return error.CltvExpiryAlreadySet;
177+
178+
self.tagged_fields.appendAssumeCapacity(.{ .min_final_cltv_expiry_delta = .{ .inner = delta } });
179+
}
180+
181+
/// Sets the payment secret and relevant features.
182+
pub fn setPaymentSecret(self: *InvoiceBuilder, gpa: std.mem.Allocator, payment_secret: PaymentSecret) !void {
183+
self.secret_flag = true;
184+
185+
var found_features = false;
186+
for (self.tagged_fields.items) |*f| {
187+
switch (f.*) {
188+
.features => |*field| {
189+
found_features = true;
190+
try field.set(Features.tlv_onion_payload_required);
191+
try field.set(Features.payment_addr_required);
192+
},
193+
else => continue,
194+
}
195+
}
196+
197+
self.tagged_fields.appendAssumeCapacity(.{ .payment_secret = payment_secret });
198+
199+
if (!found_features) {
200+
var features = Features{
201+
.flags = std.AutoHashMap(Features.FeatureBit, void).init(gpa),
202+
};
203+
204+
try features.set(Features.tlv_onion_payload_required);
205+
try features.set(Features.payment_addr_required);
206+
207+
self.tagged_fields.appendAssumeCapacity(.{ .features = features });
208+
}
209+
}
210+
211+
/// Sets the amount in millisatoshis. The optimal SI prefix is chosen automatically.
212+
pub fn setAmountMilliSatoshis(self: *InvoiceBuilder, amount_msat: u64) !void {
213+
const amount = std.math.mul(u64, amount_msat, 10) catch return error.InvalidAmount;
214+
215+
const biggest_possible_si_prefix = for (SiPrefix.valuesDesc()) |prefix| {
216+
if (amount % prefix.multiplier() == 0) break prefix;
217+
} else @panic("Pico should always match");
218+
219+
self.amount = amount / biggest_possible_si_prefix.multiplier();
220+
self.si_prefix = biggest_possible_si_prefix;
221+
}
222+
223+
test "test expiration" {
224+
var builder = try InvoiceBuilder.init(std.testing.allocator, .bitcoin);
225+
defer builder.deinit();
226+
227+
try builder.setDescription(std.testing.allocator, "Test");
228+
try builder.setPaymentHash([_]u8{0} ** 32);
229+
try builder.setPaymentSecret(
230+
std.testing.allocator,
231+
.{ .inner = [_]u8{0} ** 32 },
232+
);
233+
try builder.setTimestamp(1234567);
234+
235+
var signed_invoice = v: {
236+
var builded_raw = try builder.buildRaw(std.testing.allocator);
237+
errdefer builded_raw.deinit();
238+
239+
break :v try builded_raw.sign(std.testing.allocator, (struct {
240+
fn sign(hash: Message) !RecoverableSignature {
241+
const pk = try secp256k1.SecretKey.fromSlice(&[_]u8{41} ** 32);
242+
const secp = secp256k1.Secp256k1.genNew();
243+
defer secp.deinit();
244+
245+
return secp.signEcdsaRecoverable(&hash, &pk);
246+
}
247+
}).sign);
248+
};
249+
defer signed_invoice.deinit();
250+
251+
const invoice = try Bolt11Invoice.fromSigned(signed_invoice);
252+
253+
try std.testing.expect(invoice.wouldExpire(1234567 + invoice_lib.default_expiry_time + 1));
254+
255+
try std.testing.expect(!invoice.wouldExpire(1234567 + invoice_lib.default_expiry_time - 5));
256+
}

0 commit comments

Comments
 (0)