|
| 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