< Summary - Combined Code Coverage

Line coverage
0%
Covered lines: 0
Uncovered lines: 269
Coverable lines: 269
Total lines: 861
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, out List<FallbackAddressTaggedField> fallbackAddress)
 0199                       ? fallbackAddress.Select(x => x.Value).ToList()
 0200                       : null;
 201        }
 202        set
 203        {
 0204            _taggedFields.AddRange(value.Select(x => new FallbackAddressTaggedField(x)));
 0205        }
 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        {
 0218            return _taggedFields.TryGet<DescriptionTaggedField>(TaggedFieldTypes.Description, out var description)
 0219                       ? description.Value
 0220                       : null;
 221        }
 222        internal set
 223        {
 0224            if (value != null)
 225            {
 0226                _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        {
 0247            return _taggedFields.TryGet<PaymentSecretTaggedField>(TaggedFieldTypes.PaymentSecret, out var paymentSecret)
 0248                       ? paymentSecret.Value
 0249                       : null;
 250        }
 251        internal set
 252        {
 0253            _taggedFields.Add(new PaymentSecretTaggedField(value));
 0254        }
 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        {
 0269            return _taggedFields.TryGet<PayeePubKeyTaggedField>(TaggedFieldTypes.PayeePubKey, out var payeePubKey)
 0270                       ? payeePubKey.Value
 0271                       : null;
 272        }
 273        set
 274        {
 0275            _taggedFields.Add(new PayeePubKeyTaggedField(value));
 0276        }
 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        {
 0290            return _taggedFields
 0291                      .TryGet<DescriptionHashTaggedField>(TaggedFieldTypes.DescriptionHash, out var descriptionHash)
 0292                       ? descriptionHash.Value
 0293                       : null;
 294        }
 295        internal set
 296        {
 0297            if (value != null)
 298            {
 0299                _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        {
 0320            return _taggedFields.TryGet<MinFinalCltvExpiryTaggedField>(TaggedFieldTypes.MinFinalCltvExpiry,
 0321                                                                       out var minFinalCltvExpiry)
 0322                       ? minFinalCltvExpiry.Value
 0323                       : null;
 324        }
 325        set
 326        {
 0327            _taggedFields.Add(new MinFinalCltvExpiryTaggedField(value.Value));
 0328        }
 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        {
 0341            return _taggedFields.TryGet<MetadataTaggedField>(TaggedFieldTypes.Metadata, out var metadata)
 0342                       ? metadata.Value
 0343                       : null;
 344        }
 345        set
 346        {
 0347            if (value != null)
 348            {
 0349                _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"/>
 0376    public Invoice(LightningMoney amount, string description, uint256 paymentHash, uint256 paymentSecret,
 0377                   BitcoinNetwork bitcoinNetwork, ISecureKeyManager? secureKeyManager = null)
 378    {
 0379        _secureKeyManager = secureKeyManager;
 380
 0381        Amount = amount;
 0382        BitcoinNetwork = bitcoinNetwork;
 0383        HumanReadablePart = BuildHumanReadablePart();
 0384        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 0385        Signature = new CompactSignature(0, new byte[64]);
 386
 387        // Set Required Fields
 0388        Description = description;
 0389        PaymentHash = paymentHash;
 0390        PaymentSecret = paymentSecret;
 391
 0392        _taggedFields.Changed += OnTaggedFieldsChanged;
 0393    }
 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"/>
 0438    internal Invoice(BitcoinNetwork bitcoinNetwork, LightningMoney? amount = null, long? timestamp = null)
 439    {
 0440        Amount = amount ?? LightningMoney.Zero;
 0441        BitcoinNetwork = bitcoinNetwork;
 0442        HumanReadablePart = BuildHumanReadablePart();
 0443        Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 0444        Signature = new CompactSignature(0, new byte[64]);
 445
 0446        _taggedFields.Changed += OnTaggedFieldsChanged;
 0447    }
 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"/>
 0464    private Invoice(string invoiceString, string humanReadablePart, BitcoinNetwork bitcoinNetwork,
 0465                    LightningMoney amount,
 0466                    long timestamp, TaggedFieldList taggedFields, CompactSignature signature)
 467    {
 0468        _invoiceString = invoiceString;
 469
 0470        BitcoinNetwork = bitcoinNetwork;
 0471        HumanReadablePart = humanReadablePart;
 0472        Amount = amount;
 0473        Timestamp = timestamp;
 0474        _taggedFields = taggedFields;
 0475        Signature = signature;
 476
 0477        _taggedFields.Changed += OnTaggedFieldsChanged;
 0478    }
 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    {
 0500        return new Invoice(LightningMoney.Satoshis(amountSats), description, paymentHash, paymentSecret,
 0501                           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    {
 0533        InvoiceSerializationException.ThrowIfNullOrWhiteSpace(invoiceString);
 534
 535        try
 536        {
 0537            Bech32Encoder.DecodeLightningInvoice(invoiceString, out var data, out var signature, out var hrp);
 538
 0539            var network = GetNetwork(invoiceString);
 0540            if (expectedNetwork is not null && network != expectedNetwork)
 0541                throw new InvoiceSerializationException("Expected network does not match");
 542
 0543            var amount = ConvertHumanReadableToMilliSatoshis(hrp);
 544
 545            // Initialize the BitReader buffer
 0546            var bitReader = new BitReader(data);
 547
 0548            var timestamp = bitReader.ReadInt64FromBits(35);
 549
 0550            var taggedFields = TaggedFieldList.FromBitReader(bitReader, network);
 551
 552            // TODO: Check feature bits
 553
 0554            var invoice = new Invoice(invoiceString, hrp, network, amount, timestamp, taggedFields,
 0555                                      new CompactSignature(signature[^1], signature[..^1]));
 556
 0557            var validationResult = s_invoiceValidationService.ValidateInvoice(invoice);
 0558            if (!validationResult.IsValid)
 0559                throw new InvoiceSerializationException(string.Join(", ", validationResult.Errors));
 560
 561            // Check Signature
 0562            invoice.CheckSignature(data);
 563
 0564            return invoice;
 565        }
 0566        catch (Exception e)
 567        {
 0568            throw new InvoiceSerializationException("Error decoding invoice", e);
 569        }
 0570    }
 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
 0587            var sizeInBits = 35 + (_taggedFields.CalculateSizeInBits() * 5) + (_taggedFields.Count * 15);
 588
 589            // Initialize the BitWriter buffer
 0590            var bitWriter = new BitWriter(sizeInBits);
 591
 592            // Write the timestamp
 0593            bitWriter.WriteInt64AsBits(Timestamp, 35);
 594
 595            // Write the tagged fields
 0596            _taggedFields.WriteToBitWriter(bitWriter);
 597
 598            // Sign the invoice
 0599            var compactSignature = SignInvoice(HumanReadablePart, bitWriter, nodeKey);
 0600            var signature = new byte[compactSignature.Signature.Length + 1];
 0601            compactSignature.Signature.CopyTo(signature, 0);
 0602            signature[^1] = (byte)compactSignature.RecoveryId;
 603
 0604            var bech32Encoder = new Bech32Encoder(HumanReadablePart);
 0605            _invoiceString = bech32Encoder.EncodeLightningInvoice(bitWriter, signature);
 606
 0607            return _invoiceString;
 608        }
 0609        catch (Exception e)
 610        {
 0611            throw new InvoiceSerializationException("Error encoding invoice", e);
 612        }
 0613    }
 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    {
 0650        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode(nodeKey) : _invoiceString;
 651    }
 652
 653    #endregion
 654
 655    #region Private Methods
 656
 657    private string BuildHumanReadablePart()
 658    {
 0659        StringBuilder sb = new(InvoiceConstants.Prefix);
 0660        sb.Append(GetPrefix(BitcoinNetwork));
 0661        if (!Amount.IsZero)
 0662            ConvertAmountToHumanReadable(Amount, sb);
 663
 0664        return sb.ToString();
 665    }
 666
 667    private static string GetPrefix(BitcoinNetwork bitcoinNetwork)
 668    {
 0669        return bitcoinNetwork.Name switch
 0670        {
 0671            NetworkConstants.Mainnet => InvoiceConstants.PrefixMainet,
 0672            NetworkConstants.Testnet => InvoiceConstants.PrefixTestnet,
 0673            NetworkConstants.Regtest => InvoiceConstants.PrefixRegtest,
 0674            NetworkConstants.Signet => InvoiceConstants.PrefixSignet,
 0675            _ => throw new ArgumentException("Unsupported network type", nameof(bitcoinNetwork)),
 0676        };
 677    }
 678
 679    private static void ConvertAmountToHumanReadable(LightningMoney amount, StringBuilder sb)
 680    {
 0681        var btcAmount = amount.ToUnit(LightningMoneyUnit.Btc);
 682
 683        // Start with the smallest multiplier
 0684        var tempAmount = btcAmount * 1_000_000_000_000m; // Start with pico
 0685        char? suffix = InvoiceConstants.MultiplierPico;
 686
 687        // Try nano
 0688        if (amount.MilliSatoshi % 10 == 0)
 689        {
 0690            var nanoAmount = btcAmount * 1_000_000_000m;
 0691            if (nanoAmount == decimal.Truncate(nanoAmount))
 692            {
 0693                tempAmount = nanoAmount;
 0694                suffix = InvoiceConstants.MultiplierNano;
 695            }
 696        }
 697
 698        // Try micro
 0699        if (amount.MilliSatoshi % 1_000 == 0)
 700        {
 0701            var microAmount = btcAmount * 1_000_000m;
 0702            if (microAmount == decimal.Truncate(microAmount))
 703            {
 0704                tempAmount = microAmount;
 0705                suffix = InvoiceConstants.MultiplierMicro;
 706            }
 707        }
 708
 709        // Try milli
 0710        if (amount.MilliSatoshi % 1_000_000 == 0)
 711        {
 0712            var milliAmount = btcAmount * 1000m;
 0713            if (milliAmount == decimal.Truncate(milliAmount))
 714            {
 0715                tempAmount = milliAmount;
 0716                suffix = InvoiceConstants.MultiplierMilli;
 717            }
 718        }
 719
 720        // Try full BTC
 0721        if (amount.MilliSatoshi % 1_000_000_000 == 0)
 722        {
 0723            if (btcAmount == decimal.Truncate(btcAmount))
 724            {
 0725                tempAmount = btcAmount;
 0726                suffix = null;
 727            }
 728        }
 729
 0730        sb.Append(tempAmount.ToString("F0").TrimEnd('.'));
 0731        sb.Append(suffix);
 0732    }
 733
 734    private static ulong ConvertHumanReadableToMilliSatoshis(string humanReadablePart)
 735    {
 0736        var match = AmountRegex().Match(humanReadablePart);
 0737        if (!match.Success)
 0738            throw new ArgumentException("Invalid amount format in invoice", nameof(humanReadablePart));
 739
 0740        var amountString = match.Groups[2].Value;
 0741        var multiplier = match.Groups[3].Value;
 0742        var millisatoshis = 0ul;
 0743        if (!ulong.TryParse(amountString, out var amount))
 0744            return millisatoshis;
 745
 0746        if (multiplier == "p" && amount % 10 != 0)
 0747            throw new ArgumentException("Invalid pico amount in invoice", nameof(humanReadablePart));
 748
 749        // Calculate the millisatoshis
 0750        millisatoshis = multiplier switch
 0751        {
 0752            "m" => amount * 100_000_000,
 0753            "u" => amount * 100_000,
 0754            "n" => amount * 100,
 0755            "p" => amount / 10,
 0756            _ => amount * 100_000_000_000
 0757        };
 758
 0759        return millisatoshis;
 760    }
 761
 762    private void CheckSignature(byte[] data)
 763    {
 764        // Assemble the message (hrp + data)
 0765        var message = new byte[HumanReadablePart.Length + data.Length];
 0766        Encoding.UTF8.GetBytes(HumanReadablePart).CopyTo(message, 0);
 0767        data.CopyTo(message, HumanReadablePart.Length);
 768
 769        // Get sha256 hash of the message
 0770        var hash = new byte[CryptoConstants.Sha256HashLen];
 0771        using var sha256 = new Sha256();
 0772        sha256.AppendData(message);
 0773        sha256.GetHashAndReset(hash);
 774
 0775        var nBitcoinHash = new uint256(hash);
 776
 777        // Check if recovery is necessary
 0778        if (PayeePubKey is null)
 779        {
 0780            PayeePubKey = PubKey.RecoverCompact(nBitcoinHash, Signature);
 0781            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");
 0789    }
 790
 791    private static CompactSignature SignInvoice(string hrp, BitWriter bitWriter, Key key)
 792    {
 793        // Assemble the message (hrp + data)
 0794        var data = bitWriter.ToArray();
 0795        var message = new byte[hrp.Length + data.Length];
 0796        Encoding.UTF8.GetBytes(hrp).CopyTo(message, 0);
 0797        data.CopyTo(message, hrp.Length);
 798
 799        // Get sha256 hash of the message
 0800        var hash = new byte[CryptoConstants.Sha256HashLen];
 0801        using var sha256 = new Sha256();
 0802        sha256.AppendData(message);
 0803        sha256.GetHashAndReset(hash);
 0804        var nBitcoinHash = new uint256(hash);
 805
 806        // Sign the hash
 0807        return key.SignCompact(nBitcoinHash, false);
 0808    }
 809
 810    private static BitcoinNetwork GetNetwork(string? invoiceString)
 811    {
 0812        ArgumentException.ThrowIfNullOrWhiteSpace(invoiceString);
 813
 0814        if (invoiceString.Length < 6)
 0815            throw new ArgumentException("Invoice string is too short to extract network prefix", nameof(invoiceString));
 816
 0817        if (!s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 4), out var network)
 0818         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 3), out network)
 0819         && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 2), out network))
 0820            throw new ArgumentException("Unsupported prefix in invoice", nameof(invoiceString));
 821
 0822        return network;
 823    }
 824
 825    private void OnTaggedFieldsChanged(object? sender, EventArgs args)
 826    {
 0827        _invoiceString = null;
 0828    }
 829
 830    #endregion
 831}

/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()