< Summary - Combined Code Coverage

Line coverage
86%
Covered lines: 233
Uncovered lines: 37
Coverable lines: 270
Total lines: 891
Line coverage: 86.2%
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.02%
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
 32054    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>
 30865    public BitcoinNetwork BitcoinNetwork { get; }
 66
 67    /// <summary>
 68    /// The amount for the invoice
 69    /// </summary>
 47670    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        {
 164106            return _taggedFields.TryGet<PaymentHashTaggedField>(TaggedFieldTypes.PaymentHash, out var paymentHash)
 164107                       ? paymentHash.Value
 164108                       : null;
 109        }
 110        internal set
 111        {
 128112            _taggedFields.Add(new PaymentHashTaggedField(value));
 128113        }
 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, out List<FallbackAddressTaggedField> fallbackAddress)
 52199                       ? fallbackAddress.Select(x => x.Value).ToList()
 36200                       : null;
 201        }
 202        set
 203        {
 52204            _taggedFields.AddRange(value.Select(x => new FallbackAddressTaggedField(x)));
 24205        }
 206    }
 207
 208    /// <summary>
 209    /// The description of the invoice
 210    /// </summary>
 211    /// <remarks>
 212    /// The description is a UTF-8 encoded string that describes, in short, the purpose of payment
 213    /// </remarks>
 214    public string? Description
 215    {
 216        get
 217        {
 240218            return _taggedFields.TryGet<DescriptionTaggedField>(TaggedFieldTypes.Description, out var description)
 240219                       ? description.Value
 240220                       : null;
 221        }
 222        internal set
 223        {
 116224            if (value != null)
 225            {
 116226                _taggedFields.Add(new DescriptionTaggedField(value));
 227            }
 228            else
 229            {
 0230                _taggedFields.RemoveAll(t => t.Type.Equals(TaggedFieldTypes.Description));
 231            }
 0232        }
 233    }
 234
 235    /// <summary>
 236    /// The payment secret of the invoice
 237    /// </summary>
 238    /// <remarks>
 239    /// The payment secret is a 32-byte secret used to identify a payment
 240    /// </remarks>
 241    /// <seealso cref="uint256"/>
 242    [DisallowNull]
 243    public uint256? PaymentSecret
 244    {
 245        get
 246        {
 164247            return _taggedFields.TryGet<PaymentSecretTaggedField>(TaggedFieldTypes.PaymentSecret, out var paymentSecret)
 164248                       ? paymentSecret.Value
 164249                       : null;
 250        }
 251        internal set
 252        {
 136253            _taggedFields.Add(new PaymentSecretTaggedField(value));
 136254        }
 255    }
 256
 257    /// <summary>
 258    /// The payee pubkey of the invoice
 259    /// </summary>
 260    /// <remarks>
 261    /// The payee pubkey is the pubkey of the payee
 262    /// </remarks>
 263    /// <seealso cref="PubKey"/>
 264    [DisallowNull]
 265    public PubKey? PayeePubKey
 266    {
 267        get
 268        {
 120269            return _taggedFields.TryGet<PayeePubKeyTaggedField>(TaggedFieldTypes.PayeePubKey, out var payeePubKey)
 120270                       ? payeePubKey.Value
 120271                       : null;
 272        }
 273        set
 274        {
 64275            _taggedFields.Add(new PayeePubKeyTaggedField(value));
 64276        }
 277    }
 278
 279    /// <summary>
 280    /// The description hash of the invoice
 281    /// </summary>
 282    /// <remarks>
 283    /// The description hash is a 32-byte hash of the description
 284    /// </remarks>
 285    /// <seealso cref="uint256"/>
 286    public uint256? DescriptionHash
 287    {
 288        get
 289        {
 232290            return _taggedFields
 232291                      .TryGet<DescriptionHashTaggedField>(TaggedFieldTypes.DescriptionHash, out var descriptionHash)
 232292                       ? descriptionHash.Value
 232293                       : null;
 294        }
 295        internal set
 296        {
 32297            if (value != null)
 298            {
 32299                _taggedFields.Add(new DescriptionHashTaggedField(value));
 300            }
 301            else
 302            {
 303                // If the description hash is set to null, remove it from the tagged fields
 0304                _taggedFields.RemoveAll(x => x.Type.Equals(TaggedFieldTypes.DescriptionHash));
 305            }
 0306        }
 307    }
 308
 309    /// <summary>
 310    /// The min final cltv expiry of the invoice
 311    /// </summary>
 312    /// <remarks>
 313    /// The min final cltv expiry is the minimum final cltv expiry the payer should use
 314    /// </remarks>
 315    [DisallowNull]
 316    public ushort? MinFinalCltvExpiry
 317    {
 318        get
 319        {
 4320            return _taggedFields.TryGet<MinFinalCltvExpiryTaggedField>(TaggedFieldTypes.MinFinalCltvExpiry,
 4321                                                                       out var minFinalCltvExpiry)
 4322                       ? minFinalCltvExpiry.Value
 4323                       : null;
 324        }
 325        set
 326        {
 4327            _taggedFields.Add(new MinFinalCltvExpiryTaggedField(value.Value));
 4328        }
 329    }
 330
 331    /// <summary>
 332    /// The metadata of the invoice
 333    /// </summary>
 334    /// <remarks>
 335    /// The metadata is used to add additional information to the invoice
 336    /// </remarks>
 337    public byte[]? Metadata
 338    {
 339        get
 340        {
 4341            return _taggedFields.TryGet<MetadataTaggedField>(TaggedFieldTypes.Metadata, out var metadata)
 4342                       ? metadata.Value
 4343                       : null;
 344        }
 345        set
 346        {
 4347            if (value != null)
 348            {
 4349                _taggedFields.Add(new MetadataTaggedField(value));
 350            }
 351            else
 352            {
 0353                _taggedFields.RemoveAll(x => x.Type.Equals(TaggedFieldTypes.Metadata));
 354            }
 0355        }
 356    }
 357
 358    #endregion
 359
 360    #region Constructors
 361
 362    /// <summary>
 363    /// The base constructor for the invoice
 364    /// </summary>
 365    /// <param name="amount">The amount of the invoice</param>
 366    /// <param name="description">The description of the invoice</param>
 367    /// <param name="paymentHash">The payment hash of the invoice</param>
 368    /// <param name="paymentSecret">The payment secret of the invoice</param>
 369    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 370    /// <param name="secureKeyManager">Secure key manager</param>
 371    /// <remarks>
 372    /// The invoice is created with the given amount of millisatoshis, a description, the payment hash and the
 373    /// payment secret.
 374    /// </remarks>
 375    /// <seealso cref="BitcoinNetwork"/>
 48376    public Invoice(LightningMoney amount, string description, uint256 paymentHash, uint256 paymentSecret,
 48377                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 378    {
 48379        _secureKeyManager = secureKeyManager;
 380
 48381        Amount = amount;
 48382        BitcoinNetwork = bitcoinNetwork;
 48383        HumanReadablePart = BuildHumanReadablePart();
 48384        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 48385        Signature = new CompactSignature(0, new byte[64]);
 386
 387        // Set Required Fields
 48388        Description = description;
 48389        PaymentHash = paymentHash;
 48390        PaymentSecret = paymentSecret;
 391
 48392        _taggedFields.Changed += OnTaggedFieldsChanged;
 48393    }
 394
 395    /// <summary>
 396    /// The base constructor for the invoice
 397    /// </summary>
 398    /// <param name="amount">The amount of the invoice</param>
 399    /// <param name="descriptionHash">The description hash of the invoice</param>
 400    /// <param name="paymentHash">The payment hash of the invoice</param>
 401    /// <param name="paymentSecret">The payment secret of the invoice</param>
 402    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 403    /// <param name="secureKeyManager">Secure key manager</param>
 404    /// <remarks>
 405    /// The invoice is created with the given amount of millisatoshis, a description hash, the payment hash and the
 406    /// payment secret.
 407    /// </remarks>
 408    /// <seealso cref="BitcoinNetwork"/>
 0409    public Invoice(LightningMoney amount, uint256 descriptionHash, uint256 paymentHash, uint256 paymentSecret,
 0410                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 411    {
 0412        _secureKeyManager = secureKeyManager;
 413
 0414        Amount = amount;
 0415        BitcoinNetwork = bitcoinNetwork;
 0416        HumanReadablePart = BuildHumanReadablePart();
 0417        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 0418        Signature = new CompactSignature(0, new byte[64]);
 419
 420        // Set Required Fields
 0421        DescriptionHash = descriptionHash;
 0422        PaymentHash = paymentHash;
 0423        PaymentSecret = paymentSecret;
 424
 0425        _taggedFields.Changed += OnTaggedFieldsChanged;
 0426    }
 427
 428    /// <summary>
 429    /// This constructor is used by tests
 430    /// </summary>
 431    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 432    /// <param name="amount">The amount of the invoice</param>
 433    /// <param name="timestamp">The timestamp of the invoice</param>
 434    /// <remarks>
 435    /// The invoice is created with the given network, amount of millisatoshis and timestamp.
 436    /// </remarks>
 437    /// <seealso cref="BitcoinNetwork"/>
 204438    internal Invoice(BitcoinNetwork bitcoinNetwork, LightningMoney? amount = null, long? timestamp = null)
 439    {
 204440        Amount = amount ?? LightningMoney.Zero;
 204441        BitcoinNetwork = bitcoinNetwork;
 204442        HumanReadablePart = BuildHumanReadablePart();
 200443        Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 200444        Signature = new CompactSignature(0, new byte[64]);
 445
 200446        _taggedFields.Changed += OnTaggedFieldsChanged;
 200447    }
 448
 449    /// <summary>
 450    /// This constructor is used by Decode
 451    /// </summary>
 452    /// <param name="invoiceString">The invoice string</param>
 453    /// <param name="humanReadablePart">The human-readable part of the invoice</param>
 454    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 455    /// <param name="amount">The amount of the invoice</param>
 456    /// <param name="timestamp">The timestamp of the invoice</param>
 457    /// <param name="taggedFields">The tagged fields of the invoice</param>
 458    /// <param name="signature">The invoice signature</param>
 459    /// <remarks>
 460    /// The invoice is created with the given human-readable part, network, amount of millisatoshis,
 461    /// timestamp and tagged fields.
 462    /// </remarks>
 463    /// <seealso cref="BitcoinNetwork"/>
 68464    private Invoice(string invoiceString, string humanReadablePart, BitcoinNetwork bitcoinNetwork,
 68465                    LightningMoney amount,
 68466                    long timestamp, TaggedFieldList taggedFields, CompactSignature signature)
 467    {
 68468        _invoiceString = invoiceString;
 469
 68470        BitcoinNetwork = bitcoinNetwork;
 68471        HumanReadablePart = humanReadablePart;
 68472        Amount = amount;
 68473        Timestamp = timestamp;
 68474        _taggedFields = taggedFields;
 68475        Signature = signature;
 476
 68477        _taggedFields.Changed += OnTaggedFieldsChanged;
 68478    }
 479
 480    #endregion
 481
 482    #region Static Constructors
 483
 484    /// <summary>
 485    /// Creates a new invoice with the given amount of satoshis
 486    /// </summary>
 487    /// <param name="amountSats">The amount of satoshis the invoice is for</param>
 488    /// <param name="description">The description of the invoice</param>
 489    /// <param name="paymentHash">The payment hash of the invoice</param>
 490    /// <param name="paymentSecret">The payment secret of the invoice</param>
 491    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 492    /// <remarks>
 493    /// The invoice is created with the given amount of satoshis, a description, the payment hash and the
 494    /// payment secret.
 495    /// </remarks>
 496    /// <returns>The invoice</returns>
 497    public static Invoice InSatoshis(ulong amountSats, string description, uint256 paymentHash, uint256 paymentSecret,
 498                                     BitcoinNetwork bitcoinNetwork)
 499    {
 44500        return new Invoice(LightningMoney.Satoshis(amountSats), description, paymentHash, paymentSecret,
 44501                           bitcoinNetwork);
 502    }
 503
 504    /// <summary>
 505    /// Creates a new invoice with the given amount of satoshis
 506    /// </summary>
 507    /// <param name="amountSats">The amount of satoshis the invoice is for</param>
 508    /// <param name="descriptionHash">The description hash of the invoice</param>
 509    /// <param name="paymentHash">The payment hash of the invoice</param>
 510    /// <param name="paymentSecret">The payment secret of the invoice</param>
 511    /// <param name="bitcoinNetwork">The network the invoice is created for</param>
 512    /// <remarks>
 513    /// The invoice is created with the given amount of satoshis, a description hash, the payment hash and the
 514    /// payment secret.
 515    /// </remarks>
 516    /// <returns>The invoice</returns>
 517    public static Invoice InSatoshis(ulong amountSats, uint256 descriptionHash, uint256 paymentHash,
 518                                     uint256 paymentSecret, BitcoinNetwork bitcoinNetwork)
 519    {
 0520        return new Invoice(LightningMoney.Satoshis(amountSats), descriptionHash, paymentHash, paymentSecret,
 0521                           bitcoinNetwork);
 522    }
 523
 524    /// <summary>
 525    /// Decodes an invoice from a string
 526    /// </summary>
 527    /// <param name="invoiceString">The invoice string</param>
 528    /// <param name="expectedNetwork">The expected network of the invoice</param>
 529    /// <returns>The invoice</returns>
 530    /// <exception cref="InvoiceSerializationException">If something goes wrong in the decoding process</exception>
 531    public static Invoice Decode(string? invoiceString, BitcoinNetwork? expectedNetwork = null)
 532    {
 96533        InvoiceSerializationException.ThrowIfNullOrWhiteSpace(invoiceString);
 534
 535        try
 536        {
 92537            Bech32Encoder.DecodeLightningInvoice(invoiceString, out var data, out var signature, out var hrp);
 538
 76539            var network = GetNetwork(invoiceString);
 76540            if (expectedNetwork is not null && network != expectedNetwork)
 0541                throw new InvoiceSerializationException("Expected network does not match");
 542
 76543            var amount = ConvertHumanReadableToMilliSatoshis(hrp);
 544
 545            // Initialize the BitReader buffer
 68546            var bitReader = new BitReader(data);
 547
 68548            var timestamp = bitReader.ReadInt64FromBits(35);
 549
 68550            var taggedFields = TaggedFieldList.FromBitReader(bitReader, network);
 551
 552            // TODO: Check feature bits
 553
 68554            var invoice = new Invoice(invoiceString, hrp, network, amount, timestamp, taggedFields,
 68555                                      new CompactSignature(signature[^1], signature[..^1]));
 556
 68557            var validationResult = s_invoiceValidationService.ValidateInvoice(invoice);
 68558            if (!validationResult.IsValid)
 4559                throw new InvoiceSerializationException(string.Join(", ", validationResult.Errors));
 560
 561            // Check Signature
 64562            invoice.CheckSignature(data);
 563
 60564            return invoice;
 565        }
 32566        catch (Exception e)
 567        {
 32568            throw new InvoiceSerializationException("Error decoding invoice", e);
 569        }
 60570    }
 571
 572    #endregion
 573
 574    /// <summary>
 575    /// Encodes the current invoice into a lightning-compatible invoice format as a string.
 576    /// </summary>
 577    /// <param name="nodeKey">The private key of the node used to sign the invoice.</param>
 578    /// <returns>The encoded lightning invoice as a string.</returns>
 579    /// <exception cref="InvoiceSerializationException">
 580    /// Thrown when an error occurs during the encoding process.
 581    /// </exception>
 582    public string Encode(Key nodeKey)
 583    {
 584        try
 585        {
 586            // Calculate the size needed for the buffer
 72587            var sizeInBits = 35 + (_taggedFields.CalculateSizeInBits() * 5) + (_taggedFields.Count * 15);
 588
 589            // Initialize the BitWriter buffer
 72590            var bitWriter = new BitWriter(sizeInBits);
 591
 592            // Write the timestamp
 72593            bitWriter.WriteInt64AsBits(Timestamp, 35);
 594
 595            // Write the tagged fields
 72596            _taggedFields.WriteToBitWriter(bitWriter);
 597
 598            // Sign the invoice
 72599            var compactSignature = SignInvoice(HumanReadablePart, bitWriter, nodeKey);
 72600            var signature = new byte[compactSignature.Signature.Length + 1];
 72601            compactSignature.Signature.CopyTo(signature, 0);
 72602            signature[^1] = (byte)compactSignature.RecoveryId;
 603
 72604            var bech32Encoder = new Bech32Encoder(HumanReadablePart);
 72605            _invoiceString = bech32Encoder.EncodeLightningInvoice(bitWriter, signature);
 606
 72607            return _invoiceString;
 608        }
 0609        catch (Exception e)
 610        {
 0611            throw new InvoiceSerializationException("Error encoding invoice", e);
 612        }
 72613    }
 614
 615    /// <summary>
 616    /// Encodes the invoice into its string representation using the secure key manager.
 617    /// </summary>
 618    /// <returns>The encoded invoice string.</returns>
 619    /// <exception cref="NullReferenceException">Thrown when the secure key manager is not set.</exception>
 620    public string Encode()
 621    {
 0622        if (_secureKeyManager is null)
 0623            throw new NullReferenceException("Secure key manager is not set, please use Encode(Key nodeKey) instead");
 624
 0625        var nodeKey = _secureKeyManager.GetNodeKeyPair().PrivKey;
 0626        return Encode(new Key(nodeKey));
 627    }
 628
 629    #region Overrides
 630
 631    public override string ToString()
 632    {
 0633        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode() : _invoiceString;
 634    }
 635
 636    /// <summary>
 637    /// Converts the invoice object to its string representation.
 638    /// </summary>
 639    /// <remarks>
 640    /// If the invoice string exists, it is returned directly.
 641    /// Otherwise, the invoice is encoded using the provided node key.
 642    /// </remarks>
 643    /// <param name="nodeKey">The node key used for signing the invoice.</param>
 644    /// <returns>A string representation of the invoice.</returns>
 645    /// <exception cref="InvoiceSerializationException">
 646    /// Thrown when an error occurs during the encoding process.
 647    /// </exception>
 648    public string ToString(Key nodeKey)
 649    {
 16650        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode(nodeKey) : _invoiceString;
 651    }
 652
 653    #endregion
 654
 655    #region Private Methods
 656
 657    private string BuildHumanReadablePart()
 658    {
 252659        StringBuilder sb = new(InvoiceConstants.Prefix);
 252660        sb.Append(GetPrefix(BitcoinNetwork));
 248661        if (!Amount.IsZero)
 168662            ConvertAmountToHumanReadable(Amount, sb);
 663
 248664        return sb.ToString();
 665    }
 666
 667    private static string GetPrefix(BitcoinNetwork bitcoinNetwork)
 668    {
 252669        return bitcoinNetwork.Name switch
 252670        {
 232671            NetworkConstants.Mainnet => InvoiceConstants.PrefixMainet,
 8672            NetworkConstants.Testnet => InvoiceConstants.PrefixTestnet,
 4673            NetworkConstants.Regtest => InvoiceConstants.PrefixRegtest,
 4674            NetworkConstants.Signet => InvoiceConstants.PrefixSignet,
 4675            _ => throw new ArgumentException("Unsupported network type", nameof(bitcoinNetwork)),
 252676        };
 677    }
 678
 679    private static void ConvertAmountToHumanReadable(LightningMoney amount, StringBuilder sb)
 680    {
 168681        var btcAmount = amount.ToUnit(LightningMoneyUnit.Btc);
 682
 683        // Start with the smallest multiplier
 168684        var tempAmount = btcAmount * 1_000_000_000_000m; // Start with pico
 168685        char? suffix = InvoiceConstants.MultiplierPico;
 686
 687        // Try nano
 168688        if (amount.MilliSatoshi % 10 == 0)
 689        {
 160690            var nanoAmount = btcAmount * 1_000_000_000m;
 160691            if (nanoAmount == decimal.Truncate(nanoAmount))
 692            {
 156693                tempAmount = nanoAmount;
 156694                suffix = InvoiceConstants.MultiplierNano;
 695            }
 696        }
 697
 698        // Try micro
 168699        if (amount.MilliSatoshi % 1_000 == 0)
 700        {
 152701            var microAmount = btcAmount * 1_000_000m;
 152702            if (microAmount == decimal.Truncate(microAmount))
 703            {
 136704                tempAmount = microAmount;
 136705                suffix = InvoiceConstants.MultiplierMicro;
 706            }
 707        }
 708
 709        // Try milli
 168710        if (amount.MilliSatoshi % 1_000_000 == 0)
 711        {
 128712            var milliAmount = btcAmount * 1000m;
 128713            if (milliAmount == decimal.Truncate(milliAmount))
 714            {
 100715                tempAmount = milliAmount;
 100716                suffix = InvoiceConstants.MultiplierMilli;
 717            }
 718        }
 719
 720        // Try full BTC
 168721        if (amount.MilliSatoshi % 1_000_000_000 == 0)
 722        {
 84723            if (btcAmount == decimal.Truncate(btcAmount))
 724            {
 40725                tempAmount = btcAmount;
 40726                suffix = null;
 727            }
 728        }
 729
 168730        sb.Append(tempAmount.ToString("F0").TrimEnd('.'));
 168731        sb.Append(suffix);
 168732    }
 733
 734    private static ulong ConvertHumanReadableToMilliSatoshis(string humanReadablePart)
 735    {
 76736        var match = AmountRegex().Match(humanReadablePart);
 76737        if (!match.Success)
 4738            throw new ArgumentException("Invalid amount format in invoice", nameof(humanReadablePart));
 739
 72740        var amountString = match.Groups[2].Value;
 72741        var multiplier = match.Groups[3].Value;
 72742        var millisatoshis = 0ul;
 72743        if (!ulong.TryParse(amountString, out var amount))
 8744            return millisatoshis;
 745
 64746        if (multiplier == "p" && amount % 10 != 0)
 4747            throw new ArgumentException("Invalid pico amount in invoice", nameof(humanReadablePart));
 748
 749        // Calculate the millisatoshis
 60750        millisatoshis = multiplier switch
 60751        {
 44752            "m" => amount * 100_000_000,
 12753            "u" => amount * 100_000,
 0754            "n" => amount * 100,
 4755            "p" => amount / 10,
 0756            _ => amount * 100_000_000_000
 60757        };
 758
 60759        return millisatoshis;
 760    }
 761
 762    private void CheckSignature(byte[] data)
 763    {
 764        // Assemble the message (hrp + data)
 64765        var message = new byte[HumanReadablePart.Length + data.Length];
 64766        Encoding.UTF8.GetBytes(HumanReadablePart).CopyTo(message, 0);
 64767        data.CopyTo(message, HumanReadablePart.Length);
 768
 769        // Get sha256 hash of the message
 64770        var hash = new byte[CryptoConstants.Sha256HashLen];
 64771        using var sha256 = new Sha256();
 64772        sha256.AppendData(message);
 64773        sha256.GetHashAndReset(hash);
 774
 64775        var nBitcoinHash = new uint256(hash);
 776
 777        // Check if recovery is necessary
 64778        if (PayeePubKey is null)
 779        {
 64780            PayeePubKey = PubKey.RecoverCompact(nBitcoinHash, Signature);
 60781            return;
 782        }
 783
 0784        if (NBitcoin.Crypto.ECDSASignature.TryParseFromCompact(Signature.Signature, out var ecdsa)
 0785         && PayeePubKey.Verify(nBitcoinHash, ecdsa))
 0786            return;
 787
 0788        throw new ArgumentException("Invalid signature in invoice");
 60789    }
 790
 791    private static CompactSignature SignInvoice(string hrp, BitWriter bitWriter, Key key)
 792    {
 793        // Assemble the message (hrp + data)
 72794        var data = bitWriter.ToArray();
 72795        var message = new byte[hrp.Length + data.Length];
 72796        Encoding.UTF8.GetBytes(hrp).CopyTo(message, 0);
 72797        data.CopyTo(message, hrp.Length);
 798
 799        // Get sha256 hash of the message
 72800        var hash = new byte[CryptoConstants.Sha256HashLen];
 72801        using var sha256 = new Sha256();
 72802        sha256.AppendData(message);
 72803        sha256.GetHashAndReset(hash);
 72804        var nBitcoinHash = new uint256(hash);
 805
 806        // Sign the hash
 72807        return key.SignCompact(nBitcoinHash, false);
 72808    }
 809
 810    private static BitcoinNetwork GetNetwork(string? invoiceString)
 811    {
 76812        ArgumentException.ThrowIfNullOrWhiteSpace(invoiceString);
 813
 76814        if (invoiceString.Length < 6)
 0815            throw new ArgumentException("Invoice string is too short to extract network prefix", nameof(invoiceString));
 816
 76817        if (!s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 4), out var network)
 76818         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 3), out network)
 76819         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 2), out network))
 0820            throw new ArgumentException("Unsupported prefix in invoice", nameof(invoiceString));
 821
 76822        return network;
 823    }
 824
 825    private void OnTaggedFieldsChanged(object? sender, EventArgs args)
 826    {
 452827        _invoiceString = null;
 452828    }
 829
 830    #endregion
 831}

/home/runner/work/nlightning/nlightning/src/NLightning.Bolt11/obj/Release.Native/net9.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/net9.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/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/nlightning/nlightning/src/NLightning.Bolt11/obj/Release/net9.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()