< Summary - Combined Code Coverage

Line coverage
86%
Covered lines: 234
Uncovered lines: 37
Coverable lines: 271
Total lines: 888
Line coverage: 86.3%
Branch coverage
76%
Covered branches: 78
Total branches: 102
Branch coverage: 76.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .cctor()100%11100%
File 1: .ctor(...)71.43%1193.1%
File 1: get_BitcoinNetwork()100%11100%
File 1: get_Amount()100%11100%
File 1: get_Timestamp()100%11100%
File 1: get_Signature()100%11100%
File 1: get_HumanReadablePart()100%11100%
File 1: get_PaymentHash()100%22100%
File 1: set_PaymentHash(...)100%11100%
File 1: get_RoutingInfos()50%22100%
File 1: set_RoutingInfos(...)100%11100%
File 1: get_Features()50%22100%
File 1: set_Features(...)100%11100%
File 1: get_ExpiryDate()100%22100%
File 1: set_ExpiryDate(...)100%11100%
File 1: get_FallbackAddresses()50%22100%
File 1: set_FallbackAddresses(...)100%11100%
File 1: get_Description()100%22100%
File 1: set_Description(...)50%2.5250%
File 1: get_PaymentSecret()100%22100%
File 1: set_PaymentSecret(...)100%11100%
File 1: get_PayeePubKey()100%22100%
File 1: set_PayeePubKey(...)100%11100%
File 1: get_DescriptionHash()100%22100%
File 1: set_DescriptionHash(...)50%2.5250%
File 1: get_MinFinalCltvExpiry()50%22100%
File 1: set_MinFinalCltvExpiry(...)100%11100%
File 1: get_Metadata()50%22100%
File 1: set_Metadata(...)50%2.5250%
File 1: .ctor(...)100%210%
File 1: .ctor(...)100%44100%
File 1: .ctor(...)100%11100%
File 1: InSatoshis(...)100%11100%
File 1: InSatoshis(...)100%210%
File 1: Decode(...)75%8.01894.74%
File 1: Encode(...)100%1185.71%
File 1: Encode()0%620%
File 1: ToString()0%620%
File 1: ToString(...)50%22100%
File 1: BuildHumanReadablePart()100%22100%
File 1: GetPrefix(...)100%88100%
File 1: ConvertAmountToHumanReadable(...)100%1616100%
File 1: ConvertHumanReadableToMilliSatoshis(...)87.5%16.31689.47%
File 1: CheckSignature(...)16.67%6.56675%
File 1: SignInvoice(...)100%11100%
File 1: GetNetwork(...)75%9875%
File 1: OnTaggedFieldsChanged(...)100%11100%
File 2: AmountRegex()100%11100%
File 3: AmountRegex()100%11100%

File(s)

/home/runner/work/NLightning/NLightning/src/NLightning.Bolt11/Models/Invoice.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text;
 3using System.Text.RegularExpressions;
 4using NBitcoin;
 5
 6namespace NLightning.Bolt11.Models;
 7
 8using Domain.Constants;
 9using Domain.Crypto.Constants;
 10using Domain.Enums;
 11using Domain.Models;
 12using Domain.Money;
 13using Domain.Node;
 14using Domain.Protocol.Constants;
 15using Domain.Protocol.Interfaces;
 16using Domain.Protocol.ValueObjects;
 17using Domain.Utils;
 18using Enums;
 19using Exceptions;
 20using Infrastructure.Bitcoin.Encoders;
 21using Infrastructure.Crypto.Hashes;
 22using Services;
 23using TaggedFields;
 24
 25/// <summary>
 26/// Represents a BOLT11 Invoice
 27/// </summary>
 28/// <remarks>
 29/// The invoice is a payment request that can be sent to a payer to request a payment.
 30/// </remarks>
 31public partial class Invoice
 32{
 33    #region Private Fields
 34
 835    private static readonly InvoiceValidationService s_invoiceValidationService = new();
 36
 837    private static readonly Dictionary<string, BitcoinNetwork> s_supportedNetworks = new()
 838    {
 839        { InvoiceConstants.PrefixMainet, BitcoinNetwork.Mainnet },
 840        { InvoiceConstants.PrefixTestnet, BitcoinNetwork.Testnet },
 841        { InvoiceConstants.PrefixSignet, BitcoinNetwork.Signet },
 842        { InvoiceConstants.PrefixRegtest, BitcoinNetwork.Regtest },
 843        { InvoiceConstants.PrefixMainet.ToUpperInvariant(), BitcoinNetwork.Mainnet },
 844        { InvoiceConstants.PrefixTestnet.ToUpperInvariant(), BitcoinNetwork.Testnet },
 845        { InvoiceConstants.PrefixSignet.ToUpperInvariant(), BitcoinNetwork.Signet },
 846        { InvoiceConstants.PrefixRegtest.ToUpperInvariant(), BitcoinNetwork.Regtest }
 847    };
 48
 49    [GeneratedRegex(@"^[a-z]+((\d+)([munp])?)?$")]
 50    private static partial Regex AmountRegex();
 51
 52    private readonly ISecureKeyManager? _secureKeyManager;
 53
 32454    private readonly TaggedFieldList _taggedFields = [];
 55
 56    private string? _invoiceString;
 57
 58    #endregion
 59
 60    #region Public Properties
 61
 62    /// <summary>
 63    /// The network the invoice is created for
 64    /// </summary>
 31265    public BitcoinNetwork BitcoinNetwork { get; }
 66
 67    /// <summary>
 68    /// The amount for the invoice
 69    /// </summary>
 48070    public LightningMoney Amount { get; }
 71
 72    /// <summary>
 73    /// The timestamp of the invoice
 74    /// </summary>
 75    /// <remarks>
 76    /// The timestamp is the time the invoice was created in seconds since the Unix epoch.
 77    /// </remarks>
 16478    public long Timestamp { get; }
 79
 80    /// <summary>
 81    /// The signature of the invoice
 82    /// </summary>
 6483    public CompactSignature Signature { get; }
 84
 85    /// <summary>
 86    /// The human-readable part of the invoice
 87    /// </summary>
 45688    public string HumanReadablePart { get; }
 89
 90    #endregion
 91
 92    #region Public Properties from Tagged Fields
 93
 94    /// <summary>
 95    /// The payment hash of the invoice
 96    /// </summary>
 97    /// <remarks>
 98    /// The payment hash is a 32-byte hash used to identify a payment
 99    /// </remarks>
 100    /// <seealso cref="NBitcoin.uint256"/>
 101    [DisallowNull]
 102    public uint256? PaymentHash
 103    {
 104        get
 105        {
 168106            return _taggedFields.TryGet<PaymentHashTaggedField>(TaggedFieldTypes.PaymentHash, out var paymentHash)
 168107                       ? paymentHash.Value
 168108                       : null;
 109        }
 110        internal set
 111        {
 132112            _taggedFields.Add(new PaymentHashTaggedField(value));
 132113        }
 114    }
 115
 116    /// <summary>
 117    /// The Routing Information of the invoice
 118    /// </summary>
 119    /// <remarks>
 120    /// The routing information is used to hint about the route the payment could take
 121    /// </remarks>
 122    /// <seealso cref="RoutingInfoCollection"/>
 123    /// <seealso cref="RoutingInfo"/>
 124    [DisallowNull]
 125    public RoutingInfoCollection? RoutingInfos
 126    {
 127        get
 128        {
 100129            return _taggedFields.TryGet<RoutingInfoTaggedField>(TaggedFieldTypes.RoutingInfo, out var routingInfo)
 100130                       ? routingInfo.Value
 100131                       : null;
 132        }
 133        set
 134        {
 16135            _taggedFields.Add(new RoutingInfoTaggedField(value));
 16136            value.Changed += OnTaggedFieldsChanged;
 16137        }
 138    }
 139
 140    /// <summary>
 141    /// The features of the invoice
 142    /// </summary>
 143    /// <remarks>
 144    /// The features are used to specify the features the payer should support
 145    /// </remarks>
 146    /// <seealso cref="FeatureSet"/>
 147    [DisallowNull]
 148    public FeatureSet? Features
 149    {
 150        get
 151        {
 112152            return _taggedFields.TryGet<FeaturesTaggedField>(TaggedFieldTypes.Features, out var features)
 112153                       ? features.Value
 112154                       : null;
 155        }
 156        set
 157        {
 52158            _taggedFields.Add(new FeaturesTaggedField(value));
 52159            value.Changed += OnTaggedFieldsChanged;
 52160        }
 161    }
 162
 163    /// <summary>
 164    /// The expiry date of the invoice
 165    /// </summary>
 166    /// <remarks>
 167    /// The expiry date is the date the invoice expires
 168    /// </remarks>
 169    /// <seealso cref="DateTimeOffset"/>
 170    public DateTimeOffset ExpiryDate
 171    {
 172        get
 173        {
 20174            return _taggedFields.TryGet<ExpiryTimeTaggedField>(TaggedFieldTypes.ExpiryTime, out var expireIn)
 20175                       ? DateTimeOffset.FromUnixTimeSeconds(Timestamp + expireIn.Value)
 20176                       : DateTimeOffset.FromUnixTimeSeconds(Timestamp + InvoiceConstants.DefaultExpirationSeconds);
 177        }
 178        set
 179        {
 16180            var expireIn = value.ToUnixTimeSeconds() - Timestamp;
 16181            _taggedFields.Add(new ExpiryTimeTaggedField((int)expireIn));
 16182        }
 183    }
 184
 185    /// <summary>
 186    /// The fallback addresses of the invoice
 187    /// </summary>
 188    /// <remarks>
 189    /// The fallback addresses are used to specify the fallback addresses the payer can use
 190    /// </remarks>
 191    /// <seealso cref="BitcoinAddress"/>
 192    [DisallowNull]
 193    public List<BitcoinAddress>? FallbackAddresses
 194    {
 195        get
 196        {
 36197            return _taggedFields
 36198                      .TryGetAll(TaggedFieldTypes.FallbackAddress,
 36199                                 out List<FallbackAddressTaggedField>? fallbackAddress)
 52200                       ? fallbackAddress.Select(x => x.Value).ToList()
 36201                       : null;
 202        }
 203        set
 204        {
 52205            _taggedFields.AddRange(value.Select(x => new FallbackAddressTaggedField(x)));
 24206        }
 207    }
 208
 209    /// <summary>
 210    /// The description of the invoice
 211    /// </summary>
 212    /// <remarks>
 213    /// The description is a UTF-8 encoded string that describes, in short, the purpose of payment
 214    /// </remarks>
 215    public string? Description
 216    {
 217        get
 218        {
 244219            return _taggedFields.TryGet<DescriptionTaggedField>(TaggedFieldTypes.Description, out var description)
 244220                       ? description.Value
 244221                       : null;
 222        }
 223        internal set
 224        {
 120225            if (value != null)
 226            {
 120227                _taggedFields.Add(new DescriptionTaggedField(value));
 228            }
 229            else
 230            {
 0231                _taggedFields.RemoveAll(t => t.Type.Equals(TaggedFieldTypes.Description));
 232            }
 0233        }
 234    }
 235
 236    /// <summary>
 237    /// The payment secret of the invoice
 238    /// </summary>
 239    /// <remarks>
 240    /// The payment secret is a 32-byte secret used to identify a payment
 241    /// </remarks>
 242    /// <seealso cref="uint256"/>
 243    [DisallowNull]
 244    public uint256? PaymentSecret
 245    {
 246        get
 247        {
 168248            return _taggedFields.TryGet<PaymentSecretTaggedField>(TaggedFieldTypes.PaymentSecret, out var paymentSecret)
 168249                       ? paymentSecret.Value
 168250                       : null;
 251        }
 252        internal set
 253        {
 140254            _taggedFields.Add(new PaymentSecretTaggedField(value));
 140255        }
 256    }
 257
 258    /// <summary>
 259    /// The payee pubkey of the invoice
 260    /// </summary>
 261    /// <remarks>
 262    /// The payee pubkey is the pubkey of the payee
 263    /// </remarks>
 264    /// <seealso cref="PubKey"/>
 265    [DisallowNull]
 266    public PubKey? PayeePubKey
 267    {
 268        get
 269        {
 120270            return _taggedFields.TryGet<PayeePubKeyTaggedField>(TaggedFieldTypes.PayeePubKey, out var payeePubKey)
 120271                       ? payeePubKey.Value
 120272                       : null;
 273        }
 274        set
 275        {
 64276            _taggedFields.Add(new PayeePubKeyTaggedField(value));
 64277        }
 278    }
 279
 280    /// <summary>
 281    /// The description hash of the invoice
 282    /// </summary>
 283    /// <remarks>
 284    /// The description hash is a 32-byte hash of the description
 285    /// </remarks>
 286    /// <seealso cref="uint256"/>
 287    public uint256? DescriptionHash
 288    {
 289        get
 290        {
 236291            return _taggedFields
 236292                      .TryGet<DescriptionHashTaggedField>(TaggedFieldTypes.DescriptionHash, out var descriptionHash)
 236293                       ? descriptionHash.Value
 236294                       : null;
 295        }
 296        internal set
 297        {
 32298            if (value != null)
 299            {
 32300                _taggedFields.Add(new DescriptionHashTaggedField(value));
 301            }
 302            else
 303            {
 304                // If the description hash is set to null, remove it from the tagged fields
 0305                _taggedFields.RemoveAll(x => x.Type.Equals(TaggedFieldTypes.DescriptionHash));
 306            }
 0307        }
 308    }
 309
 310    /// <summary>
 311    /// The min final cltv expiry of the invoice
 312    /// </summary>
 313    /// <remarks>
 314    /// The min final cltv expiry is the minimum final cltv expiry the payer should use
 315    /// </remarks>
 316    [DisallowNull]
 317    public ushort? MinFinalCltvExpiry
 318    {
 319        get
 320        {
 4321            return _taggedFields.TryGet<MinFinalCltvExpiryTaggedField>(TaggedFieldTypes.MinFinalCltvExpiry,
 4322                                                                       out var minFinalCltvExpiry)
 4323                       ? minFinalCltvExpiry.Value
 4324                       : null;
 325        }
 326        set
 327        {
 4328            _taggedFields.Add(new MinFinalCltvExpiryTaggedField(value.Value));
 4329        }
 330    }
 331
 332    /// <summary>
 333    /// The metadata of the invoice
 334    /// </summary>
 335    /// <remarks>
 336    /// The metadata is used to add additional information to the invoice
 337    /// </remarks>
 338    public byte[]? Metadata
 339    {
 340        get
 341        {
 4342            return _taggedFields.TryGet<MetadataTaggedField>(TaggedFieldTypes.Metadata, out var metadata)
 4343                       ? metadata.Value
 4344                       : null;
 345        }
 346        set
 347        {
 4348            if (value != null)
 349            {
 4350                _taggedFields.Add(new MetadataTaggedField(value));
 351            }
 352            else
 353            {
 0354                _taggedFields.RemoveAll(x => x.Type.Equals(TaggedFieldTypes.Metadata));
 355            }
 0356        }
 357    }
 358
 359    #endregion
 360
 361    #region Constructors
 362
 363    /// <summary>
 364    /// The base constructor for the invoice
 365    /// </summary>
 366    /// <param name="amount">The amount of the invoice</param>
 367    /// <param name="description">The description of the invoice</param>
 368    /// <param name="paymentHash">The payment hash of the invoice</param>
 369    /// <param name="paymentSecret">The payment secret of the invoice</param>
 370    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 371    /// <param name="secureKeyManager">Secure key manager</param>
 372    /// <remarks>
 373    /// The invoice is created with the given amount of millisatoshis, a description, the payment hash and the
 374    /// payment secret.
 375    /// </remarks>
 376    /// <seealso cref="BitcoinNetwork"/>
 48377    public Invoice(LightningMoney amount, string description, uint256 paymentHash, uint256 paymentSecret,
 48378                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 379    {
 48380        _secureKeyManager = secureKeyManager;
 381
 48382        Amount = amount;
 48383        BitcoinNetwork = bitcoinNetwork;
 48384        HumanReadablePart = BuildHumanReadablePart();
 48385        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 48386        Signature = new CompactSignature(0, new byte[64]);
 387
 388        // Set Required Fields
 48389        Description = description;
 48390        PaymentHash = paymentHash;
 48391        PaymentSecret = paymentSecret;
 392
 48393        _taggedFields.Changed += OnTaggedFieldsChanged;
 48394    }
 395
 396    /// <summary>
 397    /// The base constructor for the invoice
 398    /// </summary>
 399    /// <param name="amount">The amount of the invoice</param>
 400    /// <param name="descriptionHash">The description hash of the invoice</param>
 401    /// <param name="paymentHash">The payment hash of the invoice</param>
 402    /// <param name="paymentSecret">The payment secret of the invoice</param>
 403    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 404    /// <param name="secureKeyManager">Secure key manager</param>
 405    /// <remarks>
 406    /// The invoice is created with the given amount of millisatoshis, a description hash, the payment hash and the
 407    /// payment secret.
 408    /// </remarks>
 409    /// <seealso cref="BitcoinNetwork"/>
 0410    public Invoice(LightningMoney amount, uint256 descriptionHash, uint256 paymentHash, uint256 paymentSecret,
 0411                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 412    {
 0413        _secureKeyManager = secureKeyManager;
 414
 0415        Amount = amount;
 0416        BitcoinNetwork = bitcoinNetwork;
 0417        HumanReadablePart = BuildHumanReadablePart();
 0418        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 0419        Signature = new CompactSignature(0, new byte[64]);
 420
 421        // Set Required Fields
 0422        DescriptionHash = descriptionHash;
 0423        PaymentHash = paymentHash;
 0424        PaymentSecret = paymentSecret;
 425
 0426        _taggedFields.Changed += OnTaggedFieldsChanged;
 0427    }
 428
 429    /// <summary>
 430    /// This constructor is used by tests
 431    /// </summary>
 432    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 433    /// <param name="amount">The amount of the invoice</param>
 434    /// <param name="timestamp">The timestamp of the invoice</param>
 435    /// <remarks>
 436    /// The invoice is created with the given network, amount of millisatoshis and timestamp.
 437    /// </remarks>
 438    /// <seealso cref="BitcoinNetwork"/>
 208439    internal Invoice(BitcoinNetwork bitcoinNetwork, LightningMoney? amount = null, long? timestamp = null)
 440    {
 208441        Amount = amount ?? LightningMoney.Zero;
 208442        BitcoinNetwork = bitcoinNetwork;
 208443        HumanReadablePart = BuildHumanReadablePart();
 204444        Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 204445        Signature = new CompactSignature(0, new byte[64]);
 446
 204447        _taggedFields.Changed += OnTaggedFieldsChanged;
 204448    }
 449
 450    /// <summary>
 451    /// This constructor is used by Decode
 452    /// </summary>
 453    /// <param name="invoiceString">The invoice string</param>
 454    /// <param name="humanReadablePart">The human-readable part of the invoice</param>
 455    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 456    /// <param name="amount">The amount of the invoice</param>
 457    /// <param name="timestamp">The timestamp of the invoice</param>
 458    /// <param name="taggedFields">The tagged fields of the invoice</param>
 459    /// <param name="signature">The invoice signature</param>
 460    /// <remarks>
 461    /// The invoice is created with the given human-readable part, network, amount of millisatoshis,
 462    /// timestamp and tagged fields.
 463    /// </remarks>
 464    /// <seealso cref="BitcoinNetwork"/>
 68465    private Invoice(string invoiceString, string humanReadablePart, BitcoinNetwork bitcoinNetwork,
 68466                    LightningMoney amount,
 68467                    long timestamp, TaggedFieldList taggedFields, CompactSignature signature)
 468    {
 68469        _invoiceString = invoiceString;
 470
 68471        BitcoinNetwork = bitcoinNetwork;
 68472        HumanReadablePart = humanReadablePart;
 68473        Amount = amount;
 68474        Timestamp = timestamp;
 68475        _taggedFields = taggedFields;
 68476        Signature = signature;
 477
 68478        _taggedFields.Changed += OnTaggedFieldsChanged;
 68479    }
 480
 481    #endregion
 482
 483    #region Static Constructors
 484
 485    /// <summary>
 486    /// Creates a new invoice with the given amount of satoshis
 487    /// </summary>
 488    /// <param name="amountSats">The amount of satoshis the invoice is for</param>
 489    /// <param name="description">The description of the invoice</param>
 490    /// <param name="paymentHash">The payment hash of the invoice</param>
 491    /// <param name="paymentSecret">The payment secret of the invoice</param>
 492    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 493    /// <remarks>
 494    /// The invoice is created with the given amount of satoshis, a description, the payment hash and the
 495    /// payment secret.
 496    /// </remarks>
 497    /// <returns>The invoice</returns>
 498    public static Invoice InSatoshis(ulong amountSats, string description, uint256 paymentHash, uint256 paymentSecret,
 499                                     BitcoinNetwork bitcoinNetwork)
 500    {
 44501        return new Invoice(LightningMoney.Satoshis(amountSats), description, paymentHash, paymentSecret,
 44502                           bitcoinNetwork);
 503    }
 504
 505    /// <summary>
 506    /// Creates a new invoice with the given amount of satoshis
 507    /// </summary>
 508    /// <param name="amountSats">The amount of satoshis the invoice is for</param>
 509    /// <param name="descriptionHash">The description hash of the invoice</param>
 510    /// <param name="paymentHash">The payment hash of the invoice</param>
 511    /// <param name="paymentSecret">The payment secret of the invoice</param>
 512    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 513    /// <remarks>
 514    /// The invoice is created with the given amount of satoshis, a description hash, the payment hash and the
 515    /// payment secret.
 516    /// </remarks>
 517    /// <returns>The invoice</returns>
 518    public static Invoice InSatoshis(ulong amountSats, uint256 descriptionHash, uint256 paymentHash,
 519                                     uint256 paymentSecret, BitcoinNetwork bitcoinNetwork)
 520    {
 0521        return new Invoice(LightningMoney.Satoshis(amountSats), descriptionHash, paymentHash, paymentSecret,
 0522                           bitcoinNetwork);
 523    }
 524
 525    /// <summary>
 526    /// Decodes an invoice from a string
 527    /// </summary>
 528    /// <param name="invoiceString">The invoice string</param>
 529    /// <param name="expectedNetwork">The expected network of the invoice</param>
 530    /// <returns>The invoice</returns>
 531    /// <exception cref="InvoiceSerializationException">If something goes wrong in the decoding process</exception>
 532    public static Invoice Decode(string? invoiceString, BitcoinNetwork? expectedNetwork = null)
 533    {
 96534        InvoiceSerializationException.ThrowIfNullOrWhiteSpace(invoiceString);
 535
 536        try
 537        {
 92538            Bech32Encoder.DecodeLightningInvoice(invoiceString, out var data, out var signature, out var hrp);
 539
 76540            var network = GetNetwork(invoiceString);
 76541            if (expectedNetwork is not null && network != expectedNetwork)
 0542                throw new InvoiceSerializationException("Expected network does not match");
 543
 76544            var amount = ConvertHumanReadableToMilliSatoshis(hrp);
 545
 546            // Initialize the BitReader buffer
 68547            var bitReader = new BitReader(data);
 548
 68549            var timestamp = bitReader.ReadInt64FromBits(35);
 550
 68551            var taggedFields = TaggedFieldList.FromBitReader(bitReader, network);
 552
 553            // TODO: Check feature bits
 554
 68555            var invoice = new Invoice(invoiceString, hrp, network, amount, timestamp, taggedFields,
 68556                                      new CompactSignature(signature[^1], signature[..^1]));
 557
 68558            var validationResult = s_invoiceValidationService.ValidateInvoice(invoice);
 68559            if (!validationResult.IsValid)
 4560                throw new InvoiceSerializationException(string.Join(", ", validationResult.Errors));
 561
 562            // Check Signature
 64563            invoice.CheckSignature(data);
 564
 60565            return invoice;
 566        }
 32567        catch (Exception e)
 568        {
 32569            throw new InvoiceSerializationException("Error decoding invoice", e);
 570        }
 60571    }
 572
 573    #endregion
 574
 575    /// <summary>
 576    /// Encodes the current invoice into a lightning-compatible invoice format as a string.
 577    /// </summary>
 578    /// <param name="nodeKey">The private key of the node used to sign the invoice.</param>
 579    /// <returns>The encoded lightning invoice as a string.</returns>
 580    /// <exception cref="InvoiceSerializationException">
 581    /// Thrown when an error occurs during the encoding process.
 582    /// </exception>
 583    public string Encode(Key nodeKey)
 584    {
 585        try
 586        {
 587            // Calculate the size needed for the buffer
 72588            var sizeInBits = 35 + (_taggedFields.CalculateSizeInBits() * 5) + (_taggedFields.Count * 15);
 589
 590            // Initialize the BitWriter buffer
 72591            var bitWriter = new BitWriter(sizeInBits);
 592
 593            // Write the timestamp
 72594            bitWriter.WriteInt64AsBits(Timestamp, 35);
 595
 596            // Write the tagged fields
 72597            _taggedFields.WriteToBitWriter(bitWriter);
 598
 599            // Sign the invoice
 72600            var compactSignature = SignInvoice(HumanReadablePart, bitWriter, nodeKey);
 72601            var signature = new byte[compactSignature.Signature.Length + 1];
 72602            compactSignature.Signature.CopyTo(signature, 0);
 72603            signature[^1] = (byte)compactSignature.RecoveryId;
 604
 72605            var bech32Encoder = new Bech32Encoder(HumanReadablePart);
 72606            _invoiceString = bech32Encoder.EncodeLightningInvoice(bitWriter, signature);
 607
 72608            return _invoiceString;
 609        }
 0610        catch (Exception e)
 611        {
 0612            throw new InvoiceSerializationException("Error encoding invoice", e);
 613        }
 72614    }
 615
 616    /// <summary>
 617    /// Encodes the invoice into its string representation using the secure key manager.
 618    /// </summary>
 619    /// <returns>The encoded invoice string.</returns>
 620    /// <exception cref="NullReferenceException">Thrown when the secure key manager is not set.</exception>
 621    public string Encode()
 622    {
 0623        if (_secureKeyManager is null)
 0624            throw new NullReferenceException("Secure key manager is not set, please use Encode(Key nodeKey) instead");
 625
 0626        var nodeKey = _secureKeyManager.GetNodeKeyPair().PrivKey;
 0627        return Encode(new Key(nodeKey));
 628    }
 629
 630    #region Overrides
 631
 632    public override string ToString()
 633    {
 0634        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode() : _invoiceString;
 635    }
 636
 637    /// <summary>
 638    /// Converts the invoice object to its string representation.
 639    /// </summary>
 640    /// <remarks>
 641    /// If the invoice string exists, it is returned directly.
 642    /// Otherwise, the invoice is encoded using the provided node key.
 643    /// </remarks>
 644    /// <param name="nodeKey">The node key used for signing the invoice.</param>
 645    /// <returns>A string representation of the invoice.</returns>
 646    /// <exception cref="InvoiceSerializationException">
 647    /// Thrown when an error occurs during the encoding process.
 648    /// </exception>
 649    public string ToString(Key nodeKey)
 650    {
 16651        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode(nodeKey) : _invoiceString;
 652    }
 653
 654    #endregion
 655
 656    #region Private Methods
 657
 658    private string BuildHumanReadablePart()
 659    {
 256660        StringBuilder sb = new(InvoiceConstants.Prefix);
 256661        sb.Append(GetPrefix(BitcoinNetwork));
 252662        if (!Amount.IsZero)
 168663            ConvertAmountToHumanReadable(Amount, sb);
 664
 252665        return sb.ToString();
 666    }
 667
 668    private static string GetPrefix(BitcoinNetwork bitcoinNetwork)
 669    {
 256670        return bitcoinNetwork.Name switch
 256671        {
 236672            NetworkConstants.Mainnet => InvoiceConstants.PrefixMainet,
 8673            NetworkConstants.Testnet => InvoiceConstants.PrefixTestnet,
 4674            NetworkConstants.Regtest => InvoiceConstants.PrefixRegtest,
 4675            NetworkConstants.Signet => InvoiceConstants.PrefixSignet,
 4676            _ => throw new ArgumentException("Unsupported network type", nameof(bitcoinNetwork)),
 256677        };
 678    }
 679
 680    private static void ConvertAmountToHumanReadable(LightningMoney amount, StringBuilder sb)
 681    {
 168682        var btcAmount = amount.ToUnit(LightningMoneyUnit.Btc);
 683
 684        // Start with the smallest multiplier
 168685        var tempAmount = btcAmount * 1_000_000_000_000m; // Start with pico
 168686        char? suffix = InvoiceConstants.MultiplierPico;
 687
 688        // Try nano
 168689        if (amount.MilliSatoshi % 10 == 0)
 690        {
 160691            var nanoAmount = btcAmount * 1_000_000_000m;
 160692            if (nanoAmount == decimal.Truncate(nanoAmount))
 693            {
 156694                tempAmount = nanoAmount;
 156695                suffix = InvoiceConstants.MultiplierNano;
 696            }
 697        }
 698
 699        // Try micro
 168700        if (amount.MilliSatoshi % 1_000 == 0)
 701        {
 152702            var microAmount = btcAmount * 1_000_000m;
 152703            if (microAmount == decimal.Truncate(microAmount))
 704            {
 136705                tempAmount = microAmount;
 136706                suffix = InvoiceConstants.MultiplierMicro;
 707            }
 708        }
 709
 710        // Try milli
 168711        if (amount.MilliSatoshi % 1_000_000 == 0)
 712        {
 128713            var milliAmount = btcAmount * 1000m;
 128714            if (milliAmount == decimal.Truncate(milliAmount))
 715            {
 100716                tempAmount = milliAmount;
 100717                suffix = InvoiceConstants.MultiplierMilli;
 718            }
 719        }
 720
 721        // Try full BTC
 168722        if (amount.MilliSatoshi % 1_000_000_000 == 0)
 723        {
 84724            if (btcAmount == decimal.Truncate(btcAmount))
 725            {
 40726                tempAmount = btcAmount;
 40727                suffix = null;
 728            }
 729        }
 730
 168731        sb.Append(tempAmount.ToString("F0").TrimEnd('.'));
 168732        sb.Append(suffix);
 168733    }
 734
 735    private static ulong ConvertHumanReadableToMilliSatoshis(string humanReadablePart)
 736    {
 76737        var match = AmountRegex().Match(humanReadablePart);
 76738        if (!match.Success)
 4739            throw new ArgumentException("Invalid amount format in invoice", nameof(humanReadablePart));
 740
 72741        var amountString = match.Groups[2].Value;
 72742        var multiplier = match.Groups[3].Value;
 72743        var millisatoshis = 0ul;
 72744        if (!ulong.TryParse(amountString, out var amount))
 8745            return millisatoshis;
 746
 64747        if (multiplier == "p" && amount % 10 != 0)
 4748            throw new ArgumentException("Invalid pico amount in invoice", nameof(humanReadablePart));
 749
 750        // Calculate the millisatoshis
 60751        millisatoshis = multiplier switch
 60752        {
 44753            "m" => amount * 100_000_000,
 12754            "u" => amount * 100_000,
 0755            "n" => amount * 100,
 4756            "p" => amount / 10,
 0757            _ => amount * 100_000_000_000
 60758        };
 759
 60760        return millisatoshis;
 761    }
 762
 763    private void CheckSignature(byte[] data)
 764    {
 765        // Assemble the message (hrp + data)
 64766        var message = new byte[HumanReadablePart.Length + data.Length];
 64767        Encoding.UTF8.GetBytes(HumanReadablePart).CopyTo(message, 0);
 64768        data.CopyTo(message, HumanReadablePart.Length);
 769
 770        // Get sha256 hash of the message
 64771        var hash = new byte[CryptoConstants.Sha256HashLen];
 64772        using var sha256 = new Sha256();
 64773        sha256.AppendData(message);
 64774        sha256.GetHashAndReset(hash);
 775
 64776        var nBitcoinHash = new uint256(hash);
 777
 778        // Check if recovery is necessary
 64779        if (PayeePubKey is null)
 780        {
 64781            PayeePubKey = PubKey.RecoverCompact(nBitcoinHash, Signature);
 60782            return;
 783        }
 784
 0785        if (NBitcoin.Crypto.ECDSASignature.TryParseFromCompact(Signature.Signature, out var ecdsa)
 0786         && PayeePubKey.Verify(nBitcoinHash, ecdsa))
 0787            return;
 788
 0789        throw new ArgumentException("Invalid signature in invoice");
 60790    }
 791
 792    private static CompactSignature SignInvoice(string hrp, BitWriter bitWriter, Key key)
 793    {
 794        // Assemble the message (hrp + data)
 72795        var data = bitWriter.ToArray();
 72796        var message = new byte[hrp.Length + data.Length];
 72797        Encoding.UTF8.GetBytes(hrp).CopyTo(message, 0);
 72798        data.CopyTo(message, hrp.Length);
 799
 800        // Get sha256 hash of the message
 72801        var hash = new byte[CryptoConstants.Sha256HashLen];
 72802        using var sha256 = new Sha256();
 72803        sha256.AppendData(message);
 72804        sha256.GetHashAndReset(hash);
 72805        var nBitcoinHash = new uint256(hash);
 806
 807        // Sign the hash
 72808        return key.SignCompact(nBitcoinHash, false);
 72809    }
 810
 811    private static BitcoinNetwork GetNetwork(string? invoiceString)
 812    {
 76813        ArgumentException.ThrowIfNullOrWhiteSpace(invoiceString);
 814
 76815        if (invoiceString.Length < 6)
 0816            throw new ArgumentException("Invoice string is too short to extract network prefix", nameof(invoiceString));
 817
 76818        if (!s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 4), out var network)
 76819         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 3), out network)
 76820         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 2), out network))
 0821            throw new ArgumentException("Unsupported prefix in invoice", nameof(invoiceString));
 822
 76823        return network;
 824    }
 825
 826    private void OnTaggedFieldsChanged(object? sender, EventArgs args)
 827    {
 464828        _invoiceString = null;
 464829    }
 830
 831    #endregion
 832}

/home/runner/work/NLightning/NLightning/src/NLightning.Bolt11/obj/Release.Native/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/NLightning/NLightning/src/NLightning.Bolt11/obj/Release.Native/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/NLightning/NLightning/src/NLightning.Bolt11/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/NLightning/NLightning/src/NLightning.Bolt11/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

Methods/Properties

.cctor()
.ctor(NLightning.Domain.Money.LightningMoney,System.String,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,NLightning.Domain.Protocol.Interfaces.ISecureKeyManager)
get_BitcoinNetwork()
get_Amount()
get_Timestamp()
get_Signature()
get_HumanReadablePart()
get_PaymentHash()
set_PaymentHash(NBitcoin.uint256)
get_RoutingInfos()
set_RoutingInfos(NLightning.Domain.Models.RoutingInfoCollection)
get_Features()
set_Features(NLightning.Domain.Node.FeatureSet)
get_ExpiryDate()
set_ExpiryDate(System.DateTimeOffset)
get_FallbackAddresses()
set_FallbackAddresses(System.Collections.Generic.List`1<NBitcoin.BitcoinAddress>)
get_Description()
set_Description(System.String)
get_PaymentSecret()
set_PaymentSecret(NBitcoin.uint256)
get_PayeePubKey()
set_PayeePubKey(NBitcoin.PubKey)
get_DescriptionHash()
set_DescriptionHash(NBitcoin.uint256)
get_MinFinalCltvExpiry()
set_MinFinalCltvExpiry(System.Nullable`1<System.UInt16>)
get_Metadata()
set_Metadata(System.Byte[])
.ctor(NLightning.Domain.Money.LightningMoney,NBitcoin.uint256,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,NLightning.Domain.Protocol.Interfaces.ISecureKeyManager)
.ctor(NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,NLightning.Domain.Money.LightningMoney,System.Nullable`1<System.Int64>)
.ctor(System.String,System.String,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork,NLightning.Domain.Money.LightningMoney,System.Int64,NLightning.Bolt11.Models.TaggedFieldList,NBitcoin.CompactSignature)
InSatoshis(System.UInt64,System.String,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork)
InSatoshis(System.UInt64,NBitcoin.uint256,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork)
Decode(System.String,System.Nullable`1<NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork>)
Encode(NBitcoin.Key)
Encode()
ToString()
ToString(NBitcoin.Key)
BuildHumanReadablePart()
GetPrefix(NLightning.Domain.Protocol.ValueObjects.BitcoinNetwork)
ConvertAmountToHumanReadable(NLightning.Domain.Money.LightningMoney,System.Text.StringBuilder)
ConvertHumanReadableToMilliSatoshis(System.String)
CheckSignature(System.Byte[])
SignInvoice(System.String,NLightning.Domain.Utils.BitWriter,NBitcoin.Key)
GetNetwork(System.String)
OnTaggedFieldsChanged(System.Object,System.EventArgs)
AmountRegex()
AmountRegex()