diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index c09718a53..31a4bd4f9 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -20,6 +20,7 @@ using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Hosting; using BTCPayServer.JsonConverters; +using BTCPayServer.Lightning; using BTCPayServer.Logging; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; @@ -505,7 +506,6 @@ namespace BTCPayServer.Tests var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC")); var accounting = paymentMethod.Calculate(); - Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC))); Assert.Equal(1.1m, accounting.Due); Assert.Equal(1.1m, accounting.TotalDue); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index c05fc1a4f..78aa8f569 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -19,6 +19,7 @@ using BTCPayServer.NTag424; using BTCPayServer.Payments; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; using BTCPayServer.Services.Wallets; using BTCPayServer.Views.Manage; using BTCPayServer.Views.Server; @@ -1905,7 +1906,7 @@ namespace BTCPayServer.Tests Assert.EndsWith("psbt/ready", s.Driver.Url); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url); - var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService>()).CryptoInfo.First().PaymentUrls.BIP21; + var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService>(), s.Server.PayTester.GetService()).CryptoInfo.First().PaymentUrls.BIP21; //let's make bip21 more interesting bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); diff --git a/BTCPayServer/Controllers/BitpayInvoiceController.cs b/BTCPayServer/Controllers/BitpayInvoiceController.cs index cc653e8f5..16e1e4ccd 100644 --- a/BTCPayServer/Controllers/BitpayInvoiceController.cs +++ b/BTCPayServer/Controllers/BitpayInvoiceController.cs @@ -14,6 +14,7 @@ using BTCPayServer.Models; using BTCPayServer.Payments; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitpayClient; @@ -27,14 +28,17 @@ namespace BTCPayServer.Controllers { private readonly UIInvoiceController _InvoiceController; private readonly Dictionary _bitpayExtensions; + private readonly CurrencyNameTable _currencyNameTable; private readonly InvoiceRepository _InvoiceRepository; public BitpayInvoiceController(UIInvoiceController invoiceController, Dictionary bitpayExtensions, + CurrencyNameTable currencyNameTable, InvoiceRepository invoiceRepository) { _InvoiceController = invoiceController; _bitpayExtensions = bitpayExtensions; + _currencyNameTable = currencyNameTable; _InvoiceRepository = invoiceRepository; } @@ -59,7 +63,7 @@ namespace BTCPayServer.Controllers })).FirstOrDefault(); if (invoice == null) throw new BitpayHttpException(404, "Object not found"); - return new DataWrapper(invoice.EntityToDTO(_bitpayExtensions, Url)); + return new DataWrapper(invoice.EntityToDTO(_bitpayExtensions, Url, _currencyNameTable)); } [HttpGet] [Route("invoices")] @@ -93,7 +97,7 @@ namespace BTCPayServer.Controllers }; var entities = (await _InvoiceRepository.GetInvoices(query)) - .Select((o) => o.EntityToDTO(_bitpayExtensions, Url)).ToArray(); + .Select((o) => o.EntityToDTO(_bitpayExtensions, Url, _currencyNameTable)).ToArray(); return Json(DataWrapper.Create(entities)); } @@ -103,7 +107,7 @@ namespace BTCPayServer.Controllers CancellationToken cancellationToken = default, Action entityManipulator = null) { var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator); - var resp = entity.EntityToDTO(_bitpayExtensions, Url); + var resp = entity.EntityToDTO(_bitpayExtensions, Url, _currencyNameTable); return new DataWrapper(resp) { Facade = "pos/invoice" }; } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs index 4e3a7c559..194d54c95 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs @@ -94,6 +94,8 @@ namespace BTCPayServer.Controllers.Greenfield public async Task GetNotificationSettings() { var user = await _userManager.GetUserAsync(User); + if (user is null) + return NotFound(); var model = GetNotificationSettingsData(user); return Ok(model); } @@ -103,6 +105,8 @@ namespace BTCPayServer.Controllers.Greenfield public async Task UpdateNotificationSettings(UpdateNotificationSettingsRequest request) { var user = await _userManager.GetUserAsync(User); + if (user is null) + return NotFound(); if (request.Disabled.Contains("all")) { user.DisabledNotifications = "all"; diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index e5fef03b1..e4a1e00a6 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -576,10 +576,10 @@ namespace BTCPayServer.Controllers PaymentMethodRaw = data, PaymentMethodId = paymentMethodId, PaymentMethod = paymentMethodId.ToString(), - TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency), - Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency) : null, - Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency) : null, - Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency) : null, + TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency, divisibility: data.Divisibility), + Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency, divisibility: data.Divisibility) : null, + Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency, divisibility: data.Divisibility) : null, + Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency, divisibility: data.Divisibility) : null, Address = data.Destination }; }).ToList(), @@ -876,6 +876,13 @@ namespace BTCPayServer.Controllers _paymentModelExtensions.TryGetValue(paymentMethodId, out var extension); return extension?.Image ?? ""; } + + // Show the "Common divisibility" rather than the payment method disibility. + // For example, BTC has commonly 8 digits, but on lightning it has 11. In this case, pick 8. + if (this._CurrencyNameTable.GetCurrencyData(prompt.Currency, false)?.Divisibility is not int divisibility) + divisibility = prompt.Divisibility; + + string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, divisibility); var model = new PaymentModel { Activated = prompt.Activated, @@ -893,10 +900,11 @@ namespace BTCPayServer.Controllers OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)), BtcAddress = prompt.Destination, - BtcDue = accounting.ShowMoney(accounting.Due), - BtcPaid = accounting.ShowMoney(accounting.Paid), + BtcDue = ShowMoney(accounting.Due), + BtcPaid = ShowMoney(accounting.Paid), InvoiceCurrency = invoice.Currency, - OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.PaymentMethodFee), + // The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible. + OrderAmount = ShowMoney(accounting.TotalDue - (prompt.PaymentMethodFee - prompt.TweakFee)), IsUnsetTopUp = invoice.IsUnsetTopUp(), CustomerEmail = invoice.Metadata.BuyerEmail, ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), @@ -928,7 +936,8 @@ namespace BTCPayServer.Controllers }, ReceivedConfirmations = handler is BitcoinLikePaymentHandler bh ? invoice.GetAllBitcoinPaymentData(bh, false).FirstOrDefault()?.ConfirmationCount : null, Status = invoice.Status.ToString(), - NetworkFee = prompt.PaymentMethodFee, + // The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible. + NetworkFee = prompt.PaymentMethodFee - prompt.TweakFee, IsMultiCurrency = invoice.GetPayments(false).Select(p => p.PaymentMethodId).Concat(new[] { prompt.PaymentMethodId }).Distinct().Count() > 1, StoreId = store.Id, AvailableCryptos = invoice.GetPaymentPrompts() diff --git a/BTCPayServer/HostedServices/BitpayIPNSender.cs b/BTCPayServer/HostedServices/BitpayIPNSender.cs index aeb4b4811..201a4589d 100644 --- a/BTCPayServer/HostedServices/BitpayIPNSender.cs +++ b/BTCPayServer/HostedServices/BitpayIPNSender.cs @@ -11,6 +11,7 @@ using BTCPayServer.Payments; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Mails; +using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using Microsoft.Extensions.Hosting; using MimeKit; @@ -45,6 +46,7 @@ namespace BTCPayServer.HostedServices private readonly EmailSenderFactory _EmailSenderFactory; private readonly StoreRepository _StoreRepository; private readonly Dictionary _bitpayExtensions; + private readonly CurrencyNameTable _currencyNameTable; public const string NamedClient = "bitpay-ipn"; public BitpayIPNSender( IHttpClientFactory httpClientFactory, @@ -53,6 +55,7 @@ namespace BTCPayServer.HostedServices InvoiceRepository invoiceRepository, StoreRepository storeRepository, Dictionary bitpayExtensions, + CurrencyNameTable currencyNameTable, EmailSenderFactory emailSenderFactory) { _Client = httpClientFactory.CreateClient(NamedClient); @@ -62,11 +65,12 @@ namespace BTCPayServer.HostedServices _EmailSenderFactory = emailSenderFactory; _StoreRepository = storeRepository; _bitpayExtensions = bitpayExtensions; + _currencyNameTable = currencyNameTable; } async Task Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification, bool sendMail) { - var dto = invoice.EntityToDTO(_bitpayExtensions); + var dto = invoice.EntityToDTO(_bitpayExtensions, _currencyNameTable); var notification = new InvoicePaymentNotificationEventWrapper() { Data = new InvoicePaymentNotification() diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinPaymentMethodBitpayAPIExtension.cs b/BTCPayServer/Payments/Bitcoin/BitcoinPaymentMethodBitpayAPIExtension.cs index bd94f8a50..ed3d02548 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinPaymentMethodBitpayAPIExtension.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinPaymentMethodBitpayAPIExtension.cs @@ -3,6 +3,7 @@ using System.Linq; using BTCPayServer.Models; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Mvc; using NBitpayClient; @@ -13,9 +14,11 @@ namespace BTCPayServer.Payments.Bitcoin public BitcoinPaymentMethodBitpayAPIExtension( PaymentMethodId paymentMethodId, IEnumerable paymentLinkExtensions, + CurrencyNameTable currencyNameTable, PaymentMethodHandlerDictionary handlers) { PaymentMethodId = paymentMethodId; + _currencyNameTable = currencyNameTable; paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId); handler = (BitcoinLikePaymentHandler)handlers[paymentMethodId]; } @@ -23,6 +26,16 @@ namespace BTCPayServer.Payments.Bitcoin private IPaymentLinkExtension paymentLinkExtension; private BitcoinLikePaymentHandler handler; + private readonly CurrencyNameTable _currencyNameTable; + + internal static decimal ToSmallestUnit(int divisibility, decimal v) + { + for (int i = 0; i < divisibility; i++) + { + v *= 10.0m; + } + return v; + } public void PopulateCryptoInfo(Services.Invoices.InvoiceCryptoInfo cryptoInfo, InvoiceResponse dto, PaymentPrompt prompt, IUrlHelper urlHelper) { @@ -32,8 +45,11 @@ namespace BTCPayServer.Payments.Bitcoin BIP21 = paymentLinkExtension.GetPaymentLink(prompt, urlHelper), }; var minerInfo = new MinerFeeInfo(); - minerInfo.TotalFee = accounting.ToSmallestUnit(accounting.PaymentMethodFee); + if (_currencyNameTable.GetCurrencyData(prompt.Currency, false)?.Divisibility is int divisibility) + { + minerInfo.TotalFee = ToSmallestUnit(divisibility, accounting.PaymentMethodFee); + } minerInfo.SatoshiPerBytes = handler.ParsePaymentPromptDetails(prompt.Details).RecommendedFeeRate.SatoshiPerByte; dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 532912523..ccf9f8361 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -70,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning public IOptions Options { get; } public BTCPayNetwork Network => _Network; - + static LightMoney OneSat = LightMoney.FromUnit(1.0m, LightMoneyUnit.Satoshi); public async Task ConfigurePrompt(PaymentMethodContext context) { if (context.InvoiceEntity.Type == InvoiceType.TopUp) @@ -89,15 +89,7 @@ namespace BTCPayServer.Payments.Lightning var nodeInfo = GetNodeInfo(config, context.Logs, preferOnion); var invoice = context.InvoiceEntity; - decimal due = Extensions.RoundUp(invoice.Price / paymentPrompt.Rate, paymentPrompt.Divisibility); - try - { - due = paymentPrompt.Calculate().Due; - } - catch (Exception) - { - // ignored - } + decimal due = paymentPrompt.Calculate().Due; var client = config.CreateLightningClient(_Network, Options.Value, _lightningClientFactory); var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; if (expiry < TimeSpan.Zero) @@ -116,6 +108,12 @@ namespace BTCPayServer.Payments.Lightning var request = new CreateInvoiceParams(new LightMoney(due, LightMoneyUnit.BTC), description, expiry); request.PrivateRouteHints = storeBlob.LightningPrivateRouteHints; lightningInvoice = await client.CreateInvoice(request, cts.Token); + var diff = request.Amount - lightningInvoice.Amount; + if (diff != LightMoney.Zero) + { + // Some providers doesn't round up to msat. So we tweak the fees so the due match the BOLT11's amount. + paymentPrompt.AddTweakFee(-diff.ToUnit(LightMoneyUnit.BTC)); + } } catch (OperationCanceledException) when (cts.IsCancellationRequested) { diff --git a/BTCPayServer/Services/DisplayFormatter.cs b/BTCPayServer/Services/DisplayFormatter.cs index 852a9eb62..ffe94e8ff 100644 --- a/BTCPayServer/Services/DisplayFormatter.cs +++ b/BTCPayServer/Services/DisplayFormatter.cs @@ -31,16 +31,16 @@ public class DisplayFormatter /// Currency code /// The format, defaults to amount + code, e.g. 1.234,56 USD /// Formatted amount and currency string - public string Currency(decimal value, string currency, CurrencyFormat format = CurrencyFormat.Code) + public string Currency(decimal value, string currency, CurrencyFormat format = CurrencyFormat.Code, int? divisibility = null) { var provider = _currencyNameTable.GetNumberFormatInfo(currency, true); var currencyData = _currencyNameTable.GetCurrencyData(currency, true); - var divisibility = currencyData.Divisibility; - value = value.RoundToSignificant(ref divisibility); + var div = divisibility is int d ? d : currencyData.Divisibility; + value = value.RoundToSignificant(ref div); if (divisibility != provider.CurrencyDecimalDigits) { provider = (NumberFormatInfo)provider.Clone(); - provider.CurrencyDecimalDigits = divisibility; + provider.CurrencyDecimalDigits = div; } var formatted = value.ToString("C", provider); diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 804769437..46f0d0923 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -13,6 +13,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.DataEncoders; @@ -521,11 +522,11 @@ namespace BTCPayServer.Services.Invoices return DateTimeOffset.UtcNow > ExpirationTime; } - public InvoiceResponse EntityToDTO(IDictionary bitpayExtensions) + public InvoiceResponse EntityToDTO(IDictionary bitpayExtensions, CurrencyNameTable currencyNameTable) { - return EntityToDTO(bitpayExtensions, null); + return EntityToDTO(bitpayExtensions, null, currencyNameTable); } - public InvoiceResponse EntityToDTO(IDictionary bitpayExtensions, IUrlHelper urlHelper) + public InvoiceResponse EntityToDTO(IDictionary bitpayExtensions, IUrlHelper urlHelper, CurrencyNameTable currencyNameTable) { ServerUrl = ServerUrl ?? ""; InvoiceResponse dto = new InvoiceResponse @@ -608,8 +609,11 @@ namespace BTCPayServer.Services.Invoices // is for legacy compatibility with the Bitpay API var paymentCode = GetPaymentCode(info.Currency, paymentId); dto.PaymentCodes.Add(paymentCode, cryptoInfo.PaymentUrls); - dto.PaymentSubtotals.Add(paymentCode, accounting.ToSmallestUnit(subtotalPrice)); - dto.PaymentTotals.Add(paymentCode, accounting.ToSmallestUnit(accounting.TotalDue)); + if (info.Currency is not null && currencyNameTable.GetCurrencyData(info.Currency, true)?.Divisibility is int divisibility) + { + dto.PaymentSubtotals.Add(paymentCode, BitcoinPaymentMethodBitpayAPIExtension.ToSmallestUnit(divisibility, subtotalPrice)); + dto.PaymentTotals.Add(paymentCode, BitcoinPaymentMethodBitpayAPIExtension.ToSmallestUnit(divisibility, accounting.TotalDue)); + } dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency() { Enabled = true @@ -810,7 +814,6 @@ namespace BTCPayServer.Services.Invoices public class PaymentMethodAccounting { - public int Divisibility { get; set; } /// Total amount of this invoice public decimal TotalDue { get; set; } @@ -850,23 +853,9 @@ namespace BTCPayServer.Services.Invoices /// public decimal PaymentMethodFee { get; set; } /// - /// Amount of fee already paid in the invoice's currency - /// - public decimal PaymentMethodFeeAlreadyPaid { get; set; } - /// /// Minimum required to be paid in order to accept invoice as paid /// public decimal MinimumTotalDue { get; set; } - - public decimal ToSmallestUnit(decimal v) - { - for (int i = 0; i < Divisibility; i++) - { - v *= 10.0m; - } - return v; - } - public string ShowMoney(decimal v) => MoneyExtensions.ShowMoney(v, Divisibility); } public class PaymentPrompt @@ -884,6 +873,26 @@ namespace BTCPayServer.Services.Invoices public int Divisibility { get; set; } [JsonConverter(typeof(NumericStringJsonConverter))] public decimal PaymentMethodFee { get; set; } + /// + /// A fee, hidden from UI, meant to be used when a payment method has a service provider which + /// have a different way of converting the invoice's amount into the currency of the payment method. + /// This fee can avoid under/over payments when this case happens. + /// + /// Please use so that the tweak fee is also added to the . + /// + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal TweakFee { get; set; } + /// + /// A fee, hidden from UI, meant to be used when a payment method has a service provider which + /// have a different way of converting the invoice's amount into the currency of the payment method. + /// This fee can avoid under/over payments when this case happens. + /// + /// + public void AddTweakFee(decimal value) + { + TweakFee += value; + PaymentMethodFee += value; + } public string Destination { get; set; } public JToken Details { get; set; } @@ -901,7 +910,6 @@ namespace BTCPayServer.Services.Invoices accounting.TxRequired++; grossDue += rate * PaymentMethodFee; } - accounting.Divisibility = Divisibility; accounting.TotalDue = Coins(grossDue / rate, Divisibility); accounting.Paid = Coins(i.PaidAmount.Gross / rate, Divisibility); accounting.PaymentMethodPaid = Coins(thisPaymentMethodPayments.Sum(p => p.PaidAmount.Gross), Divisibility); @@ -913,7 +921,6 @@ namespace BTCPayServer.Services.Invoices accounting.Due = Max(accounting.DueUncapped, 0.0m); accounting.PaymentMethodFee = Coins((grossDue - i.Price) / rate, Divisibility); - accounting.PaymentMethodFeeAlreadyPaid = Coins(i.PaidFee / rate, Divisibility); accounting.MinimumTotalDue = Max(Smallest(Divisibility), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / rate, Divisibility)); return accounting; diff --git a/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml index 754cb5948..c303a815a 100644 --- a/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml @@ -22,7 +22,7 @@ Type = prettyName.PrettyName(payment.PaymentMethodId), BOLT11 = offChainPaymentData.BOLT11, PaymentProof = offChainPaymentData.Preimage?.ToString(), - Amount = DisplayFormatter.Currency(payment.Value, handler.Network.CryptoCode) + Amount = DisplayFormatter.Currency(payment.Value, handler.Network.CryptoCode, divisibility: payment.Divisibility) }; }) .Where(model => model != null)