< Summary - Combined Code Coverage

Line coverage
0%
Covered lines: 0
Uncovered lines: 269
Coverable lines: 269
Total lines: 862
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 102
Branch coverage: 0%
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%210%
File 1: .ctor(...)0%210%
File 1: get_BitcoinNetwork()100%210%
File 1: get_Amount()100%210%
File 1: get_Timestamp()100%210%
File 1: get_Signature()100%210%
File 1: get_HumanReadablePart()100%210%
File 1: get_PaymentHash()0%620%
File 1: set_PaymentHash(...)100%210%
File 1: get_RoutingInfos()0%620%
File 1: set_RoutingInfos(...)100%210%
File 1: get_Features()0%620%
File 1: set_Features(...)100%210%
File 1: get_ExpiryDate()0%620%
File 1: set_ExpiryDate(...)100%210%
File 1: get_FallbackAddresses()0%620%
File 1: set_FallbackAddresses(...)100%210%
File 1: get_Description()0%620%
File 1: set_Description(...)0%620%
File 1: get_PaymentSecret()0%620%
File 1: set_PaymentSecret(...)100%210%
File 1: get_PayeePubKey()0%620%
File 1: set_PayeePubKey(...)100%210%
File 1: get_DescriptionHash()0%620%
File 1: set_DescriptionHash(...)0%620%
File 1: get_MinFinalCltvExpiry()0%620%
File 1: set_MinFinalCltvExpiry(...)100%210%
File 1: get_Metadata()0%620%
File 1: set_Metadata(...)0%620%
File 1: .ctor(...)100%210%
File 1: .ctor(...)0%2040%
File 1: .ctor(...)100%210%
File 1: InSatoshis(...)100%210%
File 1: InSatoshis(...)100%210%
File 1: Decode(...)0%7280%
File 1: Encode(...)100%210%
File 1: Encode()0%620%
File 1: ToString()0%620%
File 1: ToString(...)0%620%
File 1: BuildHumanReadablePart()0%620%
File 1: GetPrefix(...)0%7280%
File 1: ConvertAmountToHumanReadable(...)0%272160%
File 1: ConvertHumanReadableToMilliSatoshis(...)0%272160%
File 1: CheckSignature(...)0%4260%
File 1: SignInvoice(...)100%210%
File 1: GetNetwork(...)0%7280%
File 1: OnTaggedFieldsChanged(...)100%210%
File 2: AmountRegex()100%210%

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
 035    private static readonly InvoiceValidationService s_invoiceValidationService = new();
 36
 037    private static readonly Dictionary<string, BitcoinNetwork> s_supportedNetworks = new()
 038    {
 039        { InvoiceConstants.PrefixMainet, BitcoinNetwork.Mainnet },
 040        { InvoiceConstants.PrefixTestnet, BitcoinNetwork.Testnet },
 041        { InvoiceConstants.PrefixSignet, BitcoinNetwork.Signet },
 042        { InvoiceConstants.PrefixRegtest, BitcoinNetwork.Regtest },
 043        { InvoiceConstants.PrefixMainet.ToUpperInvariant(), BitcoinNetwork.Mainnet },
 044        { InvoiceConstants.PrefixTestnet.ToUpperInvariant(), BitcoinNetwork.Testnet },
 045        { InvoiceConstants.PrefixSignet.ToUpperInvariant(), BitcoinNetwork.Signet },
 046        { InvoiceConstants.PrefixRegtest.ToUpperInvariant(), BitcoinNetwork.Regtest }
 047    };
 48
 49    [GeneratedRegex(@"^[a-z]+((\d+)([munp])?)?$")]
 50    private static partial Regex AmountRegex();
 51
 52    private readonly ISecureKeyManager? _secureKeyManager;
 53
 054    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>
 065    public BitcoinNetwork BitcoinNetwork { get; }
 66
 67    /// <summary>
 68    /// The amount for the invoice
 69    /// </summary>
 070    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>
 078    public long Timestamp { get; }
 79
 80    /// <summary>
 81    /// The signature of the invoice
 82    /// </summary>
 083    public CompactSignature Signature { get; }
 84
 85    /// <summary>
 86    /// The human-readable part of the invoice
 87    /// </summary>
 088    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        {
 0106            return _taggedFields.TryGet<PaymentHashTaggedField>(TaggedFieldTypes.PaymentHash, out var paymentHash)
 0107                       ? paymentHash.Value
 0108                       : null;
 109        }
 110        internal set
 111        {
 0112            _taggedFields.Add(new PaymentHashTaggedField(value));
 0113        }
 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        {
 0129            return _taggedFields.TryGet<RoutingInfoTaggedField>(TaggedFieldTypes.RoutingInfo, out var routingInfo)
 0130                       ? routingInfo.Value
 0131                       : null;
 132        }
 133        set
 134        {
 0135            _taggedFields.Add(new RoutingInfoTaggedField(value));
 0136            value.Changed += OnTaggedFieldsChanged;
 0137        }
 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        {
 0152            return _taggedFields.TryGet<FeaturesTaggedField>(TaggedFieldTypes.Features, out var features)
 0153                       ? features.Value
 0154                       : null;
 155        }
 156        set
 157        {
 0158            _taggedFields.Add(new FeaturesTaggedField(value));
 0159            value.Changed += OnTaggedFieldsChanged;
 0160        }
 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        {
 0174            return _taggedFields.TryGet<ExpiryTimeTaggedField>(TaggedFieldTypes.ExpiryTime, out var expireIn)
 0175                       ? DateTimeOffset.FromUnixTimeSeconds(Timestamp + expireIn.Value)
 0176                       : DateTimeOffset.FromUnixTimeSeconds(Timestamp + InvoiceConstants.DefaultExpirationSeconds);
 177        }
 178        set
 179        {
 0180            var expireIn = value.ToUnixTimeSeconds() - Timestamp;
 0181            _taggedFields.Add(new ExpiryTimeTaggedField((int)expireIn));
 0182        }
 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        {
 0197            return _taggedFields
 0198                      .TryGetAll(TaggedFieldTypes.FallbackAddress,
 0199                                 out List<FallbackAddressTaggedField>? fallbackAddress)
 0200                       ? fallbackAddress.Select(x => x.Value).ToList()
 201                       : null;
 202        }
 203        set
 0204        {
 0205            _taggedFields.AddRange(value.Select(x => new FallbackAddressTaggedField(x)));
 206        }
 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
 0218        {
 0219            return _taggedFields.TryGet<DescriptionTaggedField>(TaggedFieldTypes.Description, out var description)
 0220                       ? description.Value
 221                       : null;
 222        }
 223        internal set
 0224        {
 225            if (value != null)
 0226            {
 227                _taggedFields.Add(new DescriptionTaggedField(value));
 228            }
 229            else
 0230            {
 231                _taggedFields.RemoveAll(t => t.Type.Equals(TaggedFieldTypes.Description));
 0232            }
 233        }
 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
 0247        {
 0248            return _taggedFields.TryGet<PaymentSecretTaggedField>(TaggedFieldTypes.PaymentSecret, out var paymentSecret)
 0249                       ? paymentSecret.Value
 250                       : null;
 251        }
 252        internal set
 0253        {
 0254            _taggedFields.Add(new PaymentSecretTaggedField(value));
 255        }
 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
 0269        {
 0270            return _taggedFields.TryGet<PayeePubKeyTaggedField>(TaggedFieldTypes.PayeePubKey, out var payeePubKey)
 0271                       ? payeePubKey.Value
 272                       : null;
 273        }
 274        set
 0275        {
 0276            _taggedFields.Add(new PayeePubKeyTaggedField(value));
 277        }
 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
 0290        {
 0291            return _taggedFields
 0292                      .TryGet<DescriptionHashTaggedField>(TaggedFieldTypes.DescriptionHash, out var descriptionHash)
 0293                       ? descriptionHash.Value
 294                       : null;
 295        }
 296        internal set
 0297        {
 298            if (value != null)
 0299            {
 300                _taggedFields.Add(new DescriptionHashTaggedField(value));
 301            }
 302            else
 303            {
 0304                // If the description hash is set to null, remove it from the tagged fields
 305                _taggedFields.RemoveAll(x => x.Type.Equals(TaggedFieldTypes.DescriptionHash));
 0306            }
 307        }
 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
 0320        {
 0321            return _taggedFields.TryGet<MinFinalCltvExpiryTaggedField>(TaggedFieldTypes.MinFinalCltvExpiry,
 0322                                                                       out var minFinalCltvExpiry)
 0323                       ? minFinalCltvExpiry.Value
 324                       : null;
 325        }
 326        set
 0327        {
 0328            _taggedFields.Add(new MinFinalCltvExpiryTaggedField(value.Value));
 329        }
 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
 0341        {
 0342            return _taggedFields.TryGet<MetadataTaggedField>(TaggedFieldTypes.Metadata, out var metadata)
 0343                       ? metadata.Value
 344                       : null;
 345        }
 346        set
 0347        {
 348            if (value != null)
 0349            {
 350                _taggedFields.Add(new MetadataTaggedField(value));
 351            }
 352            else
 0353            {
 354                _taggedFields.RemoveAll(x => x.Type.Equals(TaggedFieldTypes.Metadata));
 0355            }
 356        }
 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>
 0376    /// <seealso cref="BitcoinNetwork"/>
 0377    public Invoice(LightningMoney amount, string description, uint256 paymentHash, uint256 paymentSecret,
 378                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 0379    {
 380        _secureKeyManager = secureKeyManager;
 0381
 0382        Amount = amount;
 0383        BitcoinNetwork = bitcoinNetwork;
 0384        HumanReadablePart = BuildHumanReadablePart();
 0385        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 386        Signature = new CompactSignature(0, new byte[64]);
 387
 0388        // Set Required Fields
 0389        Description = description;
 0390        PaymentHash = paymentHash;
 391        PaymentSecret = paymentSecret;
 0392
 0393        _taggedFields.Changed += OnTaggedFieldsChanged;
 394    }
 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>
 0409    /// <seealso cref="BitcoinNetwork"/>
 0410    public Invoice(LightningMoney amount, uint256 descriptionHash, uint256 paymentHash, uint256 paymentSecret,
 411                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 0412    {
 413        _secureKeyManager = secureKeyManager;
 0414
 0415        Amount = amount;
 0416        BitcoinNetwork = bitcoinNetwork;
 0417        HumanReadablePart = BuildHumanReadablePart();
 0418        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 419        Signature = new CompactSignature(0, new byte[64]);
 420
 0421        // Set Required Fields
 0422        DescriptionHash = descriptionHash;
 0423        PaymentHash = paymentHash;
 424        PaymentSecret = paymentSecret;
 0425
 0426        _taggedFields.Changed += OnTaggedFieldsChanged;
 427    }
 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>
 0438    /// <seealso cref="BitcoinNetwork"/>
 439    internal Invoice(BitcoinNetwork bitcoinNetwork, LightningMoney? amount = null, long? timestamp = null)
 0440    {
 0441        Amount = amount ?? LightningMoney.Zero;
 0442        BitcoinNetwork = bitcoinNetwork;
 0443        HumanReadablePart = BuildHumanReadablePart();
 0444        Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 445        Signature = new CompactSignature(0, new byte[64]);
 0446
 0447        _taggedFields.Changed += OnTaggedFieldsChanged;
 448    }
 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>
 0464    /// <seealso cref="BitcoinNetwork"/>
 0465    private Invoice(string invoiceString, string humanReadablePart, BitcoinNetwork bitcoinNetwork,
 0466                    LightningMoney amount,
 467                    long timestamp, TaggedFieldList taggedFields, CompactSignature signature)
 0468    {
 469        _invoiceString = invoiceString;
 0470
 0471        BitcoinNetwork = bitcoinNetwork;
 0472        HumanReadablePart = humanReadablePart;
 0473        Amount = amount;
 0474        Timestamp = timestamp;
 0475        _taggedFields = taggedFields;
 476        Signature = signature;
 0477
 0478        _taggedFields.Changed += OnTaggedFieldsChanged;
 479    }
 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)
 0500    {
 0501        return new Invoice(LightningMoney.Satoshis(amountSats), description, paymentHash, paymentSecret,
 502                           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)
 0520    {
 0521        return new Invoice(LightningMoney.Satoshis(amountSats), descriptionHash, paymentHash, paymentSecret,
 522                           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)
 0533    {
 534        InvoiceSerializationException.ThrowIfNullOrWhiteSpace(invoiceString);
 535
 536        try
 0537        {
 538            Bech32Encoder.DecodeLightningInvoice(invoiceString, out var data, out var signature, out var hrp);
 0539
 0540            var network = GetNetwork(invoiceString);
 0541            if (expectedNetwork is not null && network != expectedNetwork)
 542                throw new InvoiceSerializationException("Expected network does not match");
 0543
 544            var amount = ConvertHumanReadableToMilliSatoshis(hrp);
 545
 0546            // Initialize the BitReader buffer
 547            var bitReader = new BitReader(data);
 0548
 549            var timestamp = bitReader.ReadInt64FromBits(35);
 0550
 551            var taggedFields = TaggedFieldList.FromBitReader(bitReader, network);
 552
 553            // TODO: Check feature bits
 0554
 0555            var invoice = new Invoice(invoiceString, hrp, network, amount, timestamp, taggedFields,
 556                                      new CompactSignature(signature[^1], signature[..^1]));
 0557
 0558            var validationResult = s_invoiceValidationService.ValidateInvoice(invoice);
 0559            if (!validationResult.IsValid)
 560                throw new InvoiceSerializationException(string.Join(", ", validationResult.Errors));
 561
 0562            // Check Signature
 563            invoice.CheckSignature(data);
 0564
 565            return invoice;
 0566        }
 567        catch (Exception e)
 0568        {
 569            throw new InvoiceSerializationException("Error decoding invoice", e);
 0570        }
 571    }
 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        {
 0587            // Calculate the size needed for the buffer
 588            var sizeInBits = 35 + (_taggedFields.CalculateSizeInBits() * 5) + (_taggedFields.Count * 15);
 589
 0590            // Initialize the BitWriter buffer
 591            var bitWriter = new BitWriter(sizeInBits);
 592
 0593            // Write the timestamp
 594            bitWriter.WriteInt64AsBits(Timestamp, 35);
 595
 0596            // Write the tagged fields
 597            _taggedFields.WriteToBitWriter(bitWriter);
 598
 0599            // Sign the invoice
 0600            var compactSignature = SignInvoice(HumanReadablePart, bitWriter, nodeKey);
 0601            var signature = new byte[compactSignature.Signature.Length + 1];
 0602            compactSignature.Signature.CopyTo(signature, 0);
 603            signature[^1] = (byte)compactSignature.RecoveryId;
 0604
 0605            var bech32Encoder = new Bech32Encoder(HumanReadablePart);
 606            _invoiceString = bech32Encoder.EncodeLightningInvoice(bitWriter, signature);
 0607
 608            return _invoiceString;
 0609        }
 610        catch (Exception e)
 0611        {
 612            throw new InvoiceSerializationException("Error encoding invoice", e);
 0613        }
 614    }
 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()
 0622    {
 0623        if (_secureKeyManager is null)
 624            throw new NullReferenceException("Secure key manager is not set, please use Encode(Key nodeKey) instead");
 0625
 0626        var nodeKey = _secureKeyManager.GetNodeKeyPair().PrivKey;
 627        return Encode(new Key(nodeKey));
 628    }
 629
 630    #region Overrides
 631
 632    public override string ToString()
 0633    {
 634        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)
 0650    {
 651        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode(nodeKey) : _invoiceString;
 652    }
 653
 654    #endregion
 655
 656    #region Private Methods
 657
 658    private string BuildHumanReadablePart()
 0659    {
 0660        StringBuilder sb = new(InvoiceConstants.Prefix);
 0661        sb.Append(GetPrefix(BitcoinNetwork));
 0662        if (!Amount.IsZero)
 663            ConvertAmountToHumanReadable(Amount, sb);
 0664
 665        return sb.ToString();
 666    }
 667
 668    private static string GetPrefix(BitcoinNetwork bitcoinNetwork)
 0669    {
 0670        return bitcoinNetwork.Name switch
 0671        {
 0672            NetworkConstants.Mainnet => InvoiceConstants.PrefixMainet,
 0673            NetworkConstants.Testnet => InvoiceConstants.PrefixTestnet,
 0674            NetworkConstants.Regtest => InvoiceConstants.PrefixRegtest,
 0675            NetworkConstants.Signet => InvoiceConstants.PrefixSignet,
 0676            _ => throw new ArgumentException("Unsupported network type", nameof(bitcoinNetwork)),
 677        };
 678    }
 679
 680    private static void ConvertAmountToHumanReadable(LightningMoney amount, StringBuilder sb)
 0681    {
 682        var btcAmount = amount.ToUnit(LightningMoneyUnit.Btc);
 683
 0684        // Start with the smallest multiplier
 0685        var tempAmount = btcAmount * 1_000_000_000_000m; // Start with pico
 686        char? suffix = InvoiceConstants.MultiplierPico;
 687
 0688        // Try nano
 689        if (amount.MilliSatoshi % 10 == 0)
 0690        {
 0691            var nanoAmount = btcAmount * 1_000_000_000m;
 692            if (nanoAmount == decimal.Truncate(nanoAmount))
 0693            {
 0694                tempAmount = nanoAmount;
 695                suffix = InvoiceConstants.MultiplierNano;
 696            }
 697        }
 698
 0699        // Try micro
 700        if (amount.MilliSatoshi % 1_000 == 0)
 0701        {
 0702            var microAmount = btcAmount * 1_000_000m;
 703            if (microAmount == decimal.Truncate(microAmount))
 0704            {
 0705                tempAmount = microAmount;
 706                suffix = InvoiceConstants.MultiplierMicro;
 707            }
 708        }
 709
 0710        // Try milli
 711        if (amount.MilliSatoshi % 1_000_000 == 0)
 0712        {
 0713            var milliAmount = btcAmount * 1000m;
 714            if (milliAmount == decimal.Truncate(milliAmount))
 0715            {
 0716                tempAmount = milliAmount;
 717                suffix = InvoiceConstants.MultiplierMilli;
 718            }
 719        }
 720
 0721        // Try full BTC
 722        if (amount.MilliSatoshi % 1_000_000_000 == 0)
 0723        {
 724            if (btcAmount == decimal.Truncate(btcAmount))
 0725            {
 0726                tempAmount = btcAmount;
 727                suffix = null;
 728            }
 729        }
 0730
 0731        sb.Append(tempAmount.ToString("F0").TrimEnd('.'));
 0732        sb.Append(suffix);
 733    }
 734
 735    private static ulong ConvertHumanReadableToMilliSatoshis(string humanReadablePart)
 0736    {
 0737        var match = AmountRegex().Match(humanReadablePart);
 0738        if (!match.Success)
 739            throw new ArgumentException("Invalid amount format in invoice", nameof(humanReadablePart));
 0740
 0741        var amountString = match.Groups[2].Value;
 0742        var multiplier = match.Groups[3].Value;
 0743        var millisatoshis = 0ul;
 0744        if (!ulong.TryParse(amountString, out var amount))
 745            return millisatoshis;
 0746
 0747        if (multiplier == "p" && amount % 10 != 0)
 748            throw new ArgumentException("Invalid pico amount in invoice", nameof(humanReadablePart));
 749
 0750        // Calculate the millisatoshis
 0751        millisatoshis = multiplier switch
 0752        {
 0753            "m" => amount * 100_000_000,
 0754            "u" => amount * 100_000,
 0755            "n" => amount * 100,
 0756            "p" => amount / 10,
 0757            _ => amount * 100_000_000_000
 758        };
 0759
 760        return millisatoshis;
 761    }
 762
 763    private void CheckSignature(byte[] data)
 764    {
 0765        // Assemble the message (hrp + data)
 0766        var message = new byte[HumanReadablePart.Length + data.Length];
 0767        Encoding.UTF8.GetBytes(HumanReadablePart).CopyTo(message, 0);
 768        data.CopyTo(message, HumanReadablePart.Length);
 769
 0770        // Get sha256 hash of the message
 0771        var hash = new byte[CryptoConstants.Sha256HashLen];
 0772        using var sha256 = new Sha256();
 0773        sha256.AppendData(message);
 774        sha256.GetHashAndReset(hash);
 0775
 776        var nBitcoinHash = new uint256(hash);
 777
 0778        // Check if recovery is necessary
 779        if (PayeePubKey is null)
 0780        {
 0781            PayeePubKey = PubKey.RecoverCompact(nBitcoinHash, Signature);
 782            return;
 783        }
 0784
 0785        if (NBitcoin.Crypto.ECDSASignature.TryParseFromCompact(Signature.Signature, out var ecdsa)
 0786         && PayeePubKey.Verify(nBitcoinHash, ecdsa))
 787            return;
 0788
 0789        throw new ArgumentException("Invalid signature in invoice");
 790    }
 791
 792    private static CompactSignature SignInvoice(string hrp, BitWriter bitWriter, Key key)
 793    {
 0794        // Assemble the message (hrp + data)
 0795        var data = bitWriter.ToArray();
 0796        var message = new byte[hrp.Length + data.Length];
 0797        Encoding.UTF8.GetBytes(hrp).CopyTo(message, 0);
 798        data.CopyTo(message, hrp.Length);
 799
 0800        // Get sha256 hash of the message
 0801        var hash = new byte[CryptoConstants.Sha256HashLen];
 0802        using var sha256 = new Sha256();
 0803        sha256.AppendData(message);
 0804        sha256.GetHashAndReset(hash);
 805        var nBitcoinHash = new uint256(hash);
 806
 0807        // Sign the hash
 0808        return key.SignCompact(nBitcoinHash, false);
 809    }
 810
 811    private static BitcoinNetwork GetNetwork(string? invoiceString)
 0812    {
 813        ArgumentException.ThrowIfNullOrWhiteSpace(invoiceString);
 0814
 0815        if (invoiceString.Length < 6)
 816            throw new ArgumentException("Invoice string is too short to extract network prefix", nameof(invoiceString));
 0817
 0818        if (!s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 4), out var network)
 0819         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 3), out network)
 0820         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 2), out network))
 821            throw new ArgumentException("Unsupported prefix in invoice", nameof(invoiceString));
 0822
 823        return network;
 824    }
 825
 826    private void OnTaggedFieldsChanged(object? sender, EventArgs args)
 0827    {
 0828        _invoiceString = null;
 829    }
 830
 831    #endregion
 832}

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