< Summary - Combined Code Coverage

Line coverage
89%
Covered lines: 235
Uncovered lines: 29
Coverable lines: 264
Total lines: 877
Line coverage: 89%
Branch coverage
75%
Covered branches: 85
Total branches: 112
Branch coverage: 75.8%
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: get__taggedFields()100%11100%
File 1: get_Network()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()50%22100%
File 1: set_PaymentHash(...)100%11100%
File 1: get_RoutingInfos()50%22100%
File 1: set_RoutingInfos(...)100%22100%
File 1: get_Features()50%22100%
File 1: set_Features(...)100%22100%
File 1: get_ExpiryDate()100%22100%
File 1: set_ExpiryDate(...)100%11100%
File 1: get_FallbackAddresses()50%22100%
File 1: set_FallbackAddresses(...)100%22100%
File 1: get_Description()50%22100%
File 1: set_Description(...)100%22100%
File 1: get_PaymentSecret()50%22100%
File 1: set_PaymentSecret(...)100%11100%
File 1: get_PayeePubKey()100%22100%
File 1: set_PayeePubKey(...)100%22100%
File 1: get_DescriptionHash()50%22100%
File 1: set_DescriptionHash(...)100%22100%
File 1: get_MinFinalCltvExpiry()50%22100%
File 1: set_MinFinalCltvExpiry(...)100%22100%
File 1: get_Metadata()50%22100%
File 1: set_Metadata(...)100%22100%
File 1: .ctor(...)100%11100%
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(...)50%10.141088.89%
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(...)83.33%6.17683.33%
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.Text;
 2using System.Text.RegularExpressions;
 3using NBitcoin;
 4
 5namespace NLightning.Bolt11.Models;
 6
 7using Common.Utils;
 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.Managers;
 16using Domain.ValueObjects;
 17using Enums;
 18using Exceptions;
 19using Infrastructure.Crypto.Hashes;
 20using Infrastructure.Encoders;
 21using TaggedFields;
 22
 23/// <summary>
 24/// Represents a BOLT11 Invoice
 25/// </summary>
 26/// <remarks>
 27/// The invoice is a payment request that can be sent to a payer to request a payment.
 28/// </remarks>
 29public partial class Invoice
 30{
 31    #region Private Fields
 832    private static readonly Dictionary<string, Network> s_supportedNetworks = new()
 833    {
 834        { InvoiceConstants.PREFIX_MAINET, Network.MAINNET },
 835        { InvoiceConstants.PREFIX_TESTNET, Network.TESTNET },
 836        { InvoiceConstants.PREFIX_SIGNET, Network.SIGNET },
 837        { InvoiceConstants.PREFIX_REGTEST, Network.REGTEST },
 838        { InvoiceConstants.PREFIX_MAINET.ToUpperInvariant(), Network.MAINNET },
 839        { InvoiceConstants.PREFIX_TESTNET.ToUpperInvariant(), Network.TESTNET },
 840        { InvoiceConstants.PREFIX_SIGNET.ToUpperInvariant(), Network.SIGNET },
 841        { InvoiceConstants.PREFIX_REGTEST.ToUpperInvariant(), Network.REGTEST }
 842    };
 43
 44    [GeneratedRegex(@"^[a-z]+((\d+)([munp])?)?$")]
 45    private static partial Regex AmountRegex();
 46
 47    private readonly ISecureKeyManager? _secureKeyManager;
 48
 177649    private TaggedFieldList _taggedFields { get; } = [];
 50
 51    private string? _invoiceString;
 52    #endregion
 53
 54    #region Public Properties
 55    /// <summary>
 56    /// The network the invoice is created for
 57    /// </summary>
 25258    public Network Network { get; }
 59
 60    /// <summary>
 61    /// The amount for the invoice
 62    /// </summary>
 42063    public LightningMoney Amount { get; }
 64
 65    /// <summary>
 66    /// The timestamp of the invoice
 67    /// </summary>
 68    /// <remarks>
 69    /// The timestamp is the time the invoice was created in seconds since the Unix epoch.
 70    /// </remarks>
 16471    public long Timestamp { get; }
 72
 73    /// <summary>
 74    /// The signature of the invoice
 75    /// </summary>
 6476    public CompactSignature Signature { get; }
 77
 78    /// <summary>
 79    /// The human-readable part of the invoice
 80    /// </summary>
 45681    public string HumanReadablePart { get; }
 82    #endregion
 83
 84    #region Public Properties from Tagged Fields
 85    /// <summary>
 86    /// The payment hash of the invoice
 87    /// </summary>
 88    /// <remarks>
 89    /// The payment hash is a 32 byte hash that is used to identify a payment
 90    /// </remarks>
 91    /// <seealso cref="NBitcoin.uint256"/>
 92    public uint256 PaymentHash
 93    {
 94        get
 95        {
 5696            return _taggedFields.TryGet(TaggedFieldTypes.PAYMENT_HASH, out PaymentHashTaggedField? paymentHash)
 5697                ? paymentHash!.Value
 5698                : new uint256();
 99        }
 100        internal set
 101        {
 100102            _taggedFields.Add(new PaymentHashTaggedField(value));
 100103        }
 104    }
 105
 106    /// <summary>
 107    /// The Routing Information of the invoice
 108    /// </summary>
 109    /// <remarks>
 110    /// The routing information is used to hint about the route the payment could take
 111    /// </remarks>
 112    /// <seealso cref="RoutingInfoCollection"/>
 113    /// <seealso cref="RoutingInfo"/>
 114    public RoutingInfoCollection? RoutingInfos
 115    {
 116        get
 117        {
 100118            return _taggedFields.TryGet(TaggedFieldTypes.ROUTING_INFO, out RoutingInfoTaggedField? routingInfo)
 100119                ? routingInfo!.Value
 100120                : null;
 121        }
 122        set
 123        {
 16124            if (value != null)
 125            {
 16126                _taggedFields.Add(new RoutingInfoTaggedField(value));
 16127                value.Changed += OnTaggedFieldsChanged;
 128            }
 16129        }
 130    }
 131
 132    /// <summary>
 133    /// The features of the invoice
 134    /// </summary>
 135    /// <remarks>
 136    /// The features are used to specify the features the payer should support
 137    /// </remarks>
 138    /// <seealso cref="FeatureSet"/>
 139    public FeatureSet? Features
 140    {
 141        get
 142        {
 112143            return _taggedFields.TryGet(TaggedFieldTypes.FEATURES, out FeaturesTaggedField? features)
 112144                ? features!.Value
 112145                : null;
 146        }
 147        set
 148        {
 52149            if (value != null)
 150            {
 52151                _taggedFields.Add(new FeaturesTaggedField(value));
 52152                value.Changed += OnTaggedFieldsChanged;
 153            }
 52154        }
 155    }
 156
 157    /// <summary>
 158    /// The expiry date of the invoice
 159    /// </summary>
 160    /// <remarks>
 161    /// The expiry date is the date the invoice expires
 162    /// </remarks>
 163    /// <seealso cref="DateTimeOffset"/>
 164    public DateTimeOffset ExpiryDate
 165    {
 166        get
 167        {
 20168            return _taggedFields.TryGet(TaggedFieldTypes.EXPIRY_TIME, out ExpiryTimeTaggedField? expireIn)
 20169                ? DateTimeOffset.FromUnixTimeSeconds(Timestamp + expireIn!.Value)
 20170                : DateTimeOffset.FromUnixTimeSeconds(Timestamp + InvoiceConstants.DEFAULT_EXPIRATION_SECONDS);
 171        }
 172        set
 173        {
 16174            var expireIn = value.ToUnixTimeSeconds() - Timestamp;
 16175            _taggedFields.Add(new ExpiryTimeTaggedField((int)expireIn));
 16176        }
 177    }
 178
 179    /// <summary>
 180    /// The fallback addresses of the invoice
 181    /// </summary>
 182    /// <remarks>
 183    /// The fallback addresses are used to specify the fallback addresses the payer can use
 184    /// </remarks>
 185    /// <seealso cref="BitcoinAddress"/>
 186    public List<BitcoinAddress>? FallbackAddresses
 187    {
 188        get
 189        {
 36190            return _taggedFields
 36191                .TryGetAll(TaggedFieldTypes.FALLBACK_ADDRESS, out List<FallbackAddressTaggedField> fallbackAddress)
 52192                    ? fallbackAddress.Select(x => x.Value).ToList()
 36193                    : null;
 194        }
 195        set
 196        {
 24197            if (value != null)
 198            {
 52199                _taggedFields.AddRange(value.Select(x => new FallbackAddressTaggedField(x)));
 200            }
 24201        }
 202    }
 203
 204    /// <summary>
 205    /// The description of the invoice
 206    /// </summary>
 207    /// <remarks>
 208    /// The description is a UTF-8 encoded string that describes, in short, the purpose of payment
 209    /// </remarks>
 210    public string? Description
 211    {
 212        get
 213        {
 32214            return _taggedFields.TryGet(TaggedFieldTypes.DESCRIPTION, out DescriptionTaggedField? description)
 32215                ? description!.Value
 32216                : null;
 217        }
 218        internal set
 219        {
 76220            if (value != null)
 221            {
 76222                _taggedFields.Add(new DescriptionTaggedField(value));
 223            }
 76224        }
 225    }
 226
 227    /// <summary>
 228    /// The payment secret of the invoice
 229    /// </summary>
 230    /// <remarks>
 231    /// The payment secret is a 32 byte secret that is used to identify a payment
 232    /// </remarks>
 233    /// <seealso cref="uint256"/>
 234    public uint256 PaymentSecret
 235    {
 236        get
 237        {
 56238            return _taggedFields.TryGet(TaggedFieldTypes.PAYMENT_SECRET, out PaymentSecretTaggedField? paymentSecret)
 56239                ? paymentSecret!.Value
 56240                : new uint256();
 241        }
 242        internal set
 243        {
 100244            _taggedFields.Add(new PaymentSecretTaggedField(value));
 100245        }
 246    }
 247
 248    /// <summary>
 249    /// The payee pubkey of the invoice
 250    /// </summary>
 251    /// <remarks>
 252    /// The payee pubkey is the pubkey of the payee
 253    /// </remarks>
 254    /// <seealso cref="PubKey"/>
 255    public PubKey? PayeePubKey
 256    {
 257        get
 258        {
 120259            return _taggedFields.TryGet(TaggedFieldTypes.PAYEE_PUB_KEY, out PayeePubKeyTaggedField? payeePubKey)
 120260                ? payeePubKey!.Value
 120261                : null;
 262        }
 263        set
 264        {
 64265            if (value != null)
 266            {
 64267                _taggedFields.Add(new PayeePubKeyTaggedField(value));
 268            }
 64269        }
 270    }
 271
 272    /// <summary>
 273    /// The description hash of the invoice
 274    /// </summary>
 275    /// <remarks>
 276    /// The description hash is a 32 byte hash of the description
 277    /// </remarks>
 278    /// <seealso cref="uint256"/>
 279    public uint256? DescriptionHash
 280    {
 281        get
 282        {
 24283            return _taggedFields
 24284                .TryGet(TaggedFieldTypes.DESCRIPTION_HASH, out DescriptionHashTaggedField? descriptionHash)
 24285                    ? descriptionHash!.Value
 24286                    : null;
 287        }
 288        internal set
 289        {
 24290            if (value != null)
 291            {
 24292                _taggedFields.Add(new DescriptionHashTaggedField(value));
 293            }
 24294        }
 295    }
 296
 297    /// <summary>
 298    /// The min final cltv expiry of the invoice
 299    /// </summary>
 300    /// <remarks>
 301    /// The min final cltv expiry is the minimum final cltv expiry the payer should use
 302    /// </remarks>
 303    public ushort? MinFinalCltvExpiry
 304    {
 305        get
 306        {
 4307            return _taggedFields
 4308                .TryGet(TaggedFieldTypes.MIN_FINAL_CLTV_EXPIRY, out MinFinalCltvExpiryTaggedField? minFinalCltvExpiry)
 4309                    ? minFinalCltvExpiry!.Value
 4310                    : null;
 311        }
 312        set
 313        {
 4314            if (value.HasValue)
 315            {
 4316                _taggedFields.Add(new MinFinalCltvExpiryTaggedField(value.Value));
 317            }
 4318        }
 319    }
 320
 321    /// <summary>
 322    /// The metadata of the invoice
 323    /// </summary>
 324    /// <remarks>
 325    /// The metadata is used to add additional information to the invoice
 326    /// </remarks>
 327    public byte[]? Metadata
 328    {
 329        get
 330        {
 4331            return _taggedFields.TryGet(TaggedFieldTypes.METADATA, out MetadataTaggedField? metadata)
 4332                ? metadata!.Value
 4333                : null;
 334        }
 335        set
 336        {
 4337            if (value != null)
 338            {
 4339                _taggedFields.Add(new MetadataTaggedField(value));
 340            }
 4341        }
 342    }
 343    #endregion
 344
 345    #region Constructors
 346
 347    /// <summary>
 348    /// The base constructor for the invoice
 349    /// </summary>
 350    /// <param name="amount">The amount of the invoice</param>
 351    /// <param name="description">The description of the invoice</param>
 352    /// <param name="paymentHash">The payment hash of the invoice</param>
 353    /// <param name="paymentSecret">The payment secret of the invoice</param>
 354    /// <param name="network">The network the invoice is created for</param>
 355    /// <param name="secureKeyManager">Secure key manager</param>
 356    /// <remarks>
 357    /// The invoice is created with the given amount of millisatoshis, a description, the payment hash and the
 358    /// payment secret.
 359    /// </remarks>
 360    /// <seealso cref="Network"/>
 48361    public Invoice(LightningMoney amount, string description, uint256 paymentHash, uint256 paymentSecret,
 48362                   Network network, ISecureKeyManager? secureKeyManager = null)
 363    {
 48364        _secureKeyManager = secureKeyManager;
 365
 48366        Amount = amount;
 48367        Network = network;
 48368        HumanReadablePart = BuildHumanReadablePart();
 48369        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 48370        Signature = new CompactSignature(0, new byte[64]);
 371
 372        // Set Required Fields
 48373        Description = description;
 48374        PaymentHash = paymentHash;
 48375        PaymentSecret = paymentSecret;
 376
 48377        _taggedFields.Changed += OnTaggedFieldsChanged;
 48378    }
 379
 380    /// <summary>
 381    /// The base constructor for the invoice
 382    /// </summary>
 383    /// <param name="amount">The amount of the invoice</param>
 384    /// <param name="descriptionHash">The description hash of the invoice</param>
 385    /// <param name="paymentHash">The payment hash of the invoice</param>
 386    /// <param name="paymentSecret">The payment secret of the invoice</param>
 387    /// <param name="network">The network the invoice is created for</param>
 388    /// <param name="secureKeyManager">Secure key manager</param>
 389    /// <remarks>
 390    /// The invoice is created with the given amount of millisatoshis, a description hash, the payment hash and the
 391    /// payment secret.
 392    /// </remarks>
 393    /// <seealso cref="Network"/>
 0394    public Invoice(LightningMoney amount, uint256 descriptionHash, uint256 paymentHash, uint256 paymentSecret,
 0395                   Network network, ISecureKeyManager? secureKeyManager = null)
 396    {
 0397        _secureKeyManager = secureKeyManager;
 398
 0399        Amount = amount;
 0400        Network = network;
 0401        HumanReadablePart = BuildHumanReadablePart();
 0402        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 0403        Signature = new CompactSignature(0, new byte[64]);
 404
 405        // Set Required Fields
 0406        DescriptionHash = descriptionHash;
 0407        PaymentHash = paymentHash;
 0408        PaymentSecret = paymentSecret;
 409
 0410        _taggedFields.Changed += OnTaggedFieldsChanged;
 0411    }
 412
 413    /// <summary>
 414    /// This constructor is used by tests
 415    /// </summary>
 416    /// <param name="network">The network the invoice is created for</param>
 417    /// <param name="amount">The amount of the invoice</param>
 418    /// <param name="timestamp">The timestamp of the invoice</param>
 419    /// <remarks>
 420    /// The invoice is created with the given network, amount of millisatoshis and timestamp.
 421    /// </remarks>
 422    /// <seealso cref="Network"/>
 148423    internal Invoice(Network network, LightningMoney? amount = null, long? timestamp = null)
 424    {
 148425        Amount = amount ?? LightningMoney.Zero;
 148426        Network = network;
 148427        HumanReadablePart = BuildHumanReadablePart();
 144428        Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds();
 144429        Signature = new CompactSignature(0, new byte[64]);
 430
 144431        _taggedFields.Changed += OnTaggedFieldsChanged;
 144432    }
 433
 434    /// <summary>
 435    /// This constructor is used by Decode
 436    /// </summary>
 437    /// <param name="invoiceString">The invoice string</param>
 438    /// <param name="humanReadablePart">The human-readable part of the invoice</param>
 439    /// <param name="network">The network the invoice is created for</param>
 440    /// <param name="amount">The amount of the invoice</param>
 441    /// <param name="timestamp">The timestamp of the invoice</param>
 442    /// <param name="taggedFields">The tagged fields of the invoice</param>
 443    /// <param name="signature">The invoice signature</param>
 444    /// <remarks>
 445    /// The invoice is created with the given human-readable part, network, amount of millisatoshis,
 446    /// timestamp and tagged fields.
 447    /// </remarks>
 448    /// <seealso cref="Network"/>
 64449    private Invoice(string invoiceString, string humanReadablePart, Network network, LightningMoney amount,
 64450                    long timestamp, TaggedFieldList taggedFields, CompactSignature signature)
 451    {
 64452        _invoiceString = invoiceString;
 453
 64454        Network = network;
 64455        HumanReadablePart = humanReadablePart;
 64456        Amount = amount;
 64457        Timestamp = timestamp;
 64458        _taggedFields = taggedFields;
 64459        Signature = signature;
 460
 64461        _taggedFields.Changed += OnTaggedFieldsChanged;
 64462    }
 463    #endregion
 464
 465    #region Static Constructors
 466
 467    /// <summary>
 468    /// Creates a new invoice with the given amount of satoshis
 469    /// </summary>
 470    /// <param name="amountSats">The amount of satoshis the invoice is for</param>
 471    /// <param name="description">The description of the invoice</param>
 472    /// <param name="paymentHash">The payment hash of the invoice</param>
 473    /// <param name="paymentSecret">The payment secret of the invoice</param>
 474    /// <param name="network">The network the invoice is created for</param>
 475    /// <remarks>
 476    /// The invoice is created with the given amount of satoshis, a description, the payment hash and the
 477    /// payment secret.
 478    /// </remarks>
 479    /// <returns>The invoice</returns>
 480    public static Invoice InSatoshis(ulong amountSats, string description, uint256 paymentHash, uint256 paymentSecret,
 481                                     Network network)
 482    {
 44483        return new Invoice(LightningMoney.Satoshis(amountSats), description, paymentHash, paymentSecret, network);
 484    }
 485
 486    /// <summary>
 487    /// Creates a new invoice with the given amount of satoshis
 488    /// </summary>
 489    /// <param name="amountSats">The amount of satoshis the invoice is for</param>
 490    /// <param name="descriptionHash">The description hash of the invoice</param>
 491    /// <param name="paymentHash">The payment hash of the invoice</param>
 492    /// <param name="paymentSecret">The payment secret of the invoice</param>
 493    /// <param name="network">The network the invoice is created for</param>
 494    /// <remarks>
 495    /// The invoice is created with the given amount of satoshis, a description hash, the payment hash and the
 496    /// payment secret.
 497    /// </remarks>
 498    /// <returns>The invoice</returns>
 499    public static Invoice InSatoshis(ulong amountSats, uint256 descriptionHash, uint256 paymentHash,
 500                                     uint256 paymentSecret, Network network)
 501    {
 0502        return new Invoice(LightningMoney.Satoshis(amountSats), descriptionHash, paymentHash, paymentSecret, network);
 503    }
 504
 505    /// <summary>
 506    /// Decodes an invoice from a string
 507    /// </summary>
 508    /// <param name="invoiceString">The invoice string</param>
 509    /// <param name="expectedNetwork">The expected network of the invoice</param>
 510    /// <returns>The invoice</returns>
 511    /// <exception cref="InvoiceSerializationException">If something goes wrong in the decoding process</exception>
 512    public static Invoice Decode(string? invoiceString, Network? expectedNetwork = null)
 513    {
 92514        InvoiceSerializationException.ThrowIfNullOrWhiteSpace(invoiceString);
 515
 516        try
 517        {
 88518            Bech32Encoder.DecodeLightningInvoice(invoiceString, out var data, out var signature, out var hrp);
 519
 72520            var network = GetNetwork(invoiceString);
 72521            if (expectedNetwork != null && network != expectedNetwork)
 522            {
 0523                throw new InvoiceSerializationException("Expected network does not match");
 524            }
 525
 72526            var amount = ConvertHumanReadableToMilliSatoshis(hrp);
 527
 528            // Initialize the BitReader buffer
 64529            var bitReader = new BitReader(data);
 530
 64531            var timestamp = bitReader.ReadInt64FromBits(35);
 532
 64533            var taggedFields = TaggedFieldList.FromBitReader(bitReader, network);
 534
 535            // TODO: Check feature bits
 536
 64537            var invoice = new Invoice(invoiceString, hrp, network, amount, timestamp, taggedFields,
 64538                                      new CompactSignature(signature[^1], signature[..^1]));
 539
 540            // Get pubkey from tagged fields
 64541            if (taggedFields.TryGet(TaggedFieldTypes.PAYEE_PUB_KEY, out PayeePubKeyTaggedField? pubkeyTaggedField))
 542            {
 0543                invoice.PayeePubKey = pubkeyTaggedField?.Value;
 544            }
 545            // Check Signature
 64546            invoice.CheckSignature(data);
 547
 60548            return invoice;
 549        }
 28550        catch (Exception e)
 551        {
 28552            throw new InvoiceSerializationException("Error decoding invoice", e);
 553        }
 60554    }
 555    #endregion
 556
 557    /// <summary>
 558    /// Encodes the current invoice into a lightning-compatible invoice format as a string.
 559    /// </summary>
 560    /// <param name="nodeKey">The private key of the node used to sign the invoice.</param>
 561    /// <returns>The encoded lightning invoice as a string.</returns>
 562    /// <exception cref="InvoiceSerializationException">
 563    /// Thrown when an error occurs during the encoding process.
 564    /// </exception>
 565    public string Encode(Key nodeKey)
 566    {
 567        try
 568        {
 569            // Calculate the size needed for the buffer
 72570            var sizeInBits = 35 + (_taggedFields.CalculateSizeInBits() * 5) + (_taggedFields.Count * 15);
 571
 572            // Initialize the BitWriter buffer
 72573            var bitWriter = new BitWriter(sizeInBits);
 574
 575            // Write the timestamp
 72576            bitWriter.WriteInt64AsBits(Timestamp, 35);
 577
 578            // Write the tagged fields
 72579            _taggedFields.WriteToBitWriter(bitWriter);
 580
 581            // Sign the invoice
 72582            var compactSignature = SignInvoice(HumanReadablePart, bitWriter, nodeKey);
 72583            var signature = new byte[compactSignature.Signature.Length + 1];
 72584            compactSignature.Signature.CopyTo(signature, 0);
 72585            signature[^1] = (byte)compactSignature.RecoveryId;
 586
 72587            var bech32Encoder = new Bech32Encoder(HumanReadablePart);
 72588            _invoiceString = bech32Encoder.EncodeLightningInvoice(bitWriter, signature);
 589
 72590            return _invoiceString;
 591        }
 0592        catch (Exception e)
 593        {
 0594            throw new InvoiceSerializationException("Error encoding invoice", e);
 595        }
 72596    }
 597
 598    /// <summary>
 599    /// Encodes the invoice into its string representation using the secure key manager.
 600    /// </summary>
 601    /// <returns>The encoded invoice string.</returns>
 602    /// <exception cref="NullReferenceException">Thrown when the secure key manager is not set.</exception>
 603    public string Encode()
 604    {
 0605        if (_secureKeyManager is null)
 0606            throw new NullReferenceException("Secure key manager is not set, please use Encode(Key nodeKey) instead");
 607
 0608        return Encode(_secureKeyManager.GetNodeKey());
 609    }
 610
 611    #region Overrides
 612    public override string ToString()
 613    {
 0614        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode() : _invoiceString;
 615    }
 616
 617    /// <summary>
 618    /// Converts the invoice object to its string representation.
 619    /// </summary>
 620    /// <remarks>
 621    /// If the invoice string exists, it is returned directly.
 622    /// Otherwise, the invoice is encoded using the provided node key.
 623    /// </remarks>
 624    /// <param name="nodeKey">The node key used for signing the invoice.</param>
 625    /// <returns>A string representation of the invoice.</returns>
 626    /// <exception cref="InvoiceSerializationException">
 627    /// Thrown when an error occurs during the encoding process.
 628    /// </exception>
 629    public string ToString(Key nodeKey)
 630    {
 16631        return string.IsNullOrWhiteSpace(_invoiceString) ? Encode(nodeKey) : _invoiceString;
 632    }
 633    #endregion
 634
 635    #region Private Methods
 636    private string BuildHumanReadablePart()
 637    {
 196638        StringBuilder sb = new(InvoiceConstants.PREFIX);
 196639        sb.Append(GetPrefix(Network));
 192640        if (!Amount.IsZero)
 641        {
 168642            ConvertAmountToHumanReadable(Amount, sb);
 643        }
 192644        return sb.ToString();
 645    }
 646
 647    private static string GetPrefix(Network network)
 648    {
 196649        return network.Name switch
 196650        {
 176651            NetworkConstants.MAINNET => InvoiceConstants.PREFIX_MAINET,
 8652            NetworkConstants.TESTNET => InvoiceConstants.PREFIX_TESTNET,
 4653            NetworkConstants.REGTEST => InvoiceConstants.PREFIX_REGTEST,
 4654            NetworkConstants.SIGNET => InvoiceConstants.PREFIX_SIGNET,
 4655            _ => throw new ArgumentException("Unsupported network type", nameof(network)),
 196656        };
 657    }
 658
 659    private static void ConvertAmountToHumanReadable(LightningMoney amount, StringBuilder sb)
 660    {
 168661        var btcAmount = amount.ToUnit(LightningMoneyUnit.Btc);
 662
 663        // Start with the smallest multiplier
 168664        var tempAmount = btcAmount * 1_000_000_000_000m; // Start with pico
 168665        char? suffix = InvoiceConstants.MULTIPLIER_PICO;
 666
 667        // Try nano
 168668        if (amount.MilliSatoshi % 10 == 0)
 669        {
 160670            var nanoAmount = btcAmount * 1_000_000_000m;
 160671            if (nanoAmount == decimal.Truncate(nanoAmount))
 672            {
 156673                tempAmount = nanoAmount;
 156674                suffix = InvoiceConstants.MULTIPLIER_NANO;
 675            }
 676        }
 677
 678        // Try micro
 168679        if (amount.MilliSatoshi % 1_000 == 0)
 680        {
 152681            var microAmount = btcAmount * 1_000_000m;
 152682            if (microAmount == decimal.Truncate(microAmount))
 683            {
 136684                tempAmount = microAmount;
 136685                suffix = InvoiceConstants.MULTIPLIER_MICRO;
 686            }
 687        }
 688
 689        // Try milli
 168690        if (amount.MilliSatoshi % 1_000_000 == 0)
 691        {
 128692            var milliAmount = btcAmount * 1000m;
 128693            if (milliAmount == decimal.Truncate(milliAmount))
 694            {
 100695                tempAmount = milliAmount;
 100696                suffix = InvoiceConstants.MULTIPLIER_MILLI;
 697            }
 698        }
 699
 700        // Try full BTC
 168701        if (amount.MilliSatoshi % 1_000_000_000 == 0)
 702        {
 84703            if (btcAmount == decimal.Truncate(btcAmount))
 704            {
 40705                tempAmount = btcAmount;
 40706                suffix = null;
 707            }
 708        }
 709
 168710        sb.Append(tempAmount.ToString("F0").TrimEnd('.'));
 168711        sb.Append(suffix);
 168712    }
 713
 714    private static ulong ConvertHumanReadableToMilliSatoshis(string humanReadablePart)
 715    {
 72716        var match = AmountRegex().Match(humanReadablePart);
 72717        if (!match.Success)
 718        {
 4719            throw new ArgumentException("Invalid amount format in invoice", nameof(humanReadablePart));
 720        }
 721
 68722        var amountString = match.Groups[2].Value;
 68723        var multiplier = match.Groups[3].Value;
 68724        var millisatoshis = 0ul;
 68725        if (!ulong.TryParse(amountString, out var amount))
 726        {
 4727            return millisatoshis;
 728        }
 729
 64730        if (multiplier == "p" && amount % 10 != 0)
 731        {
 4732            throw new ArgumentException("Invalid pico amount in invoice", nameof(humanReadablePart));
 733        }
 734
 735        // Calculate the millisatoshis
 60736        millisatoshis = multiplier switch
 60737        {
 44738            "m" => amount * 100_000_000,
 12739            "u" => amount * 100_000,
 0740            "n" => amount * 100,
 4741            "p" => amount / 10,
 0742            _ => amount * 100_000_000_000
 60743        };
 744
 60745        return millisatoshis;
 746    }
 747
 748    private void CheckSignature(byte[] data)
 749    {
 750        // Assemble the message (hrp + data)
 64751        var message = new byte[HumanReadablePart.Length + data.Length];
 64752        Encoding.UTF8.GetBytes(HumanReadablePart).CopyTo(message, 0);
 64753        data.CopyTo(message, HumanReadablePart.Length);
 754
 755        // Get sha256 hash of the message
 64756        var hash = new byte[CryptoConstants.SHA256_HASH_LEN];
 64757        using var sha256 = new Sha256();
 64758        sha256.AppendData(message);
 64759        sha256.GetHashAndReset(hash);
 760
 64761        var nBitcoinHash = new uint256(hash);
 762
 763        // Check if recovery is necessary
 64764        if (PayeePubKey == null)
 765        {
 64766            PayeePubKey = PubKey.RecoverCompact(nBitcoinHash, Signature);
 60767            return;
 768        }
 769
 0770        if (NBitcoin.Crypto.ECDSASignature.TryParseFromCompact(Signature.Signature, out var ecdsa)
 0771            && PayeePubKey.Verify(nBitcoinHash, ecdsa))
 772        {
 0773            return;
 774        }
 775
 0776        throw new ArgumentException("Invalid signature in invoice");
 60777    }
 778
 779    private static CompactSignature SignInvoice(string hrp, BitWriter bitWriter, Key key)
 780    {
 781        // Assemble the message (hrp + data)
 72782        var data = bitWriter.ToArray();
 72783        var message = new byte[hrp.Length + data.Length];
 72784        Encoding.UTF8.GetBytes(hrp).CopyTo(message, 0);
 72785        data.CopyTo(message, hrp.Length);
 786
 787        // Get sha256 hash of the message
 72788        var hash = new byte[CryptoConstants.SHA256_HASH_LEN];
 72789        using var sha256 = new Sha256();
 72790        sha256.AppendData(message);
 72791        sha256.GetHashAndReset(hash);
 72792        var nBitcoinHash = new uint256(hash);
 793
 794        // Sign the hash
 72795        return key.SignCompact(nBitcoinHash, false);
 72796    }
 797
 798    private static Network GetNetwork(string? invoiceString)
 799    {
 72800        ArgumentException.ThrowIfNullOrWhiteSpace(invoiceString);
 801
 72802        if (!s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 4), out var network)
 72803            && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 3), out network)
 72804            && !s_supportedNetworks.TryGetValue(invoiceString.Substring(2, 2), out network))
 805        {
 0806            throw new ArgumentException("Unsupported prefix in invoice", nameof(invoiceString));
 807        }
 808
 72809        return network;
 810    }
 811
 812    private void OnTaggedFieldsChanged(object? sender, EventArgs args)
 813    {
 340814        _invoiceString = null;
 340815    }
 816    #endregion
 817}

/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()
get__taggedFields()
get_Network()
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,System.String,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.ValueObjects.Network,NLightning.Domain.Protocol.Managers.ISecureKeyManager)
.ctor(NLightning.Domain.Money.LightningMoney,NBitcoin.uint256,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.ValueObjects.Network,NLightning.Domain.Protocol.Managers.ISecureKeyManager)
.ctor(NLightning.Domain.ValueObjects.Network,NLightning.Domain.Money.LightningMoney,System.Nullable`1<System.Int64>)
.ctor(System.String,System.String,NLightning.Domain.ValueObjects.Network,NLightning.Domain.Money.LightningMoney,System.Int64,NLightning.Bolt11.Models.TaggedFieldList,NBitcoin.CompactSignature)
InSatoshis(System.UInt64,System.String,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.ValueObjects.Network)
InSatoshis(System.UInt64,NBitcoin.uint256,NBitcoin.uint256,NBitcoin.uint256,NLightning.Domain.ValueObjects.Network)
Decode(System.String,System.Nullable`1<NLightning.Domain.ValueObjects.Network>)
Encode(NBitcoin.Key)
Encode()
ToString()
ToString(NBitcoin.Key)
BuildHumanReadablePart()
GetPrefix(NLightning.Domain.ValueObjects.Network)
ConvertAmountToHumanReadable(NLightning.Domain.Money.LightningMoney,System.Text.StringBuilder)
ConvertHumanReadableToMilliSatoshis(System.String)
CheckSignature(System.Byte[])
SignInvoice(System.String,NLightning.Common.Utils.BitWriter,NBitcoin.Key)
GetNetwork(System.String)
OnTaggedFieldsChanged(System.Object,System.EventArgs)
AmountRegex()
AmountRegex()