mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Fix divisibility in invoice details of lightning amounts (#6202)
* Fix divisibility in invoice details of lightning amounts This PR will show 11 decimal in the invoice details for BTC amount of lightning payment methods. It also hacks around the fact that some lightning clients don't create the requested amount of sats, which resulted in over or under payments. (Blink not supporting msats, and strike) Now, In that case, a payment method fee (which can be negative) called tweak fee will be added to the prompt. We are also hiding this tweak fee from the user in the checkout page in order to not disturb the UI with inconsequential fee of 0.000000001 sats. * Only show 8 digits in checkout, even if amount is 11 digits
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>()).CryptoInfo.First().PaymentUrls.BIP21;
|
||||
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).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);
|
||||
|
||||
@@ -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<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
public BitpayInvoiceController(UIInvoiceController invoiceController,
|
||||
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> 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<InvoiceResponse>(invoice.EntityToDTO(_bitpayExtensions, Url));
|
||||
return new DataWrapper<InvoiceResponse>(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<InvoiceEntity> 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<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> 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<IActionResult> UpdateNotificationSettings(UpdateNotificationSettingsRequest request)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user is null)
|
||||
return NotFound();
|
||||
if (request.Disabled.Contains("all"))
|
||||
{
|
||||
user.DisabledNotifications = "all";
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _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<PaymentMethodId, IPaymentMethodBitpayAPIExtension> 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()
|
||||
|
||||
@@ -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<IPaymentLinkExtension> 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);
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public IOptions<LightningNetworkOptions> 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)
|
||||
{
|
||||
|
||||
@@ -31,16 +31,16 @@ public class DisplayFormatter
|
||||
/// <param name="currency">Currency code</param>
|
||||
/// <param name="format">The format, defaults to amount + code, e.g. 1.234,56 USD</param>
|
||||
/// <returns>Formatted amount and currency string</returns>
|
||||
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);
|
||||
|
||||
|
||||
@@ -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<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions)
|
||||
public InvoiceResponse EntityToDTO(IDictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions, CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
return EntityToDTO(bitpayExtensions, null);
|
||||
return EntityToDTO(bitpayExtensions, null, currencyNameTable);
|
||||
}
|
||||
public InvoiceResponse EntityToDTO(IDictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions, IUrlHelper urlHelper)
|
||||
public InvoiceResponse EntityToDTO(IDictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> 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; }
|
||||
/// <summary>Total amount of this invoice</summary>
|
||||
public decimal TotalDue { get; set; }
|
||||
|
||||
@@ -850,23 +853,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// </summary>
|
||||
public decimal PaymentMethodFee { get; set; }
|
||||
/// <summary>
|
||||
/// Amount of fee already paid in the invoice's currency
|
||||
/// </summary>
|
||||
public decimal PaymentMethodFeeAlreadyPaid { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum required to be paid in order to accept invoice as paid
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 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 <see cref="AddTweakFee(decimal)"/> so that the tweak fee is also added to the <see cref="PaymentMethodFee"/>.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal TweakFee { get; set; }
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user