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.HostedServices;
|
||||||
using BTCPayServer.Hosting;
|
using BTCPayServer.Hosting;
|
||||||
using BTCPayServer.JsonConverters;
|
using BTCPayServer.JsonConverters;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Bitcoin;
|
using BTCPayServer.Payments.Bitcoin;
|
||||||
@@ -505,7 +506,6 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
||||||
var accounting = paymentMethod.Calculate();
|
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.Due);
|
||||||
Assert.Equal(1.1m, accounting.TotalDue);
|
Assert.Equal(1.1m, accounting.TotalDue);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using BTCPayServer.NTag424;
|
|||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Views.Manage;
|
using BTCPayServer.Views.Manage;
|
||||||
using BTCPayServer.Views.Server;
|
using BTCPayServer.Views.Server;
|
||||||
@@ -1905,7 +1906,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
||||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||||
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
|
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
|
//let's make bip21 more interesting
|
||||||
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
||||||
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using BTCPayServer.Models;
|
|||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Security.Greenfield;
|
using BTCPayServer.Security.Greenfield;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
@@ -27,14 +28,17 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
private readonly UIInvoiceController _InvoiceController;
|
private readonly UIInvoiceController _InvoiceController;
|
||||||
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
||||||
|
private readonly CurrencyNameTable _currencyNameTable;
|
||||||
private readonly InvoiceRepository _InvoiceRepository;
|
private readonly InvoiceRepository _InvoiceRepository;
|
||||||
|
|
||||||
public BitpayInvoiceController(UIInvoiceController invoiceController,
|
public BitpayInvoiceController(UIInvoiceController invoiceController,
|
||||||
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
|
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
|
||||||
|
CurrencyNameTable currencyNameTable,
|
||||||
InvoiceRepository invoiceRepository)
|
InvoiceRepository invoiceRepository)
|
||||||
{
|
{
|
||||||
_InvoiceController = invoiceController;
|
_InvoiceController = invoiceController;
|
||||||
_bitpayExtensions = bitpayExtensions;
|
_bitpayExtensions = bitpayExtensions;
|
||||||
|
_currencyNameTable = currencyNameTable;
|
||||||
_InvoiceRepository = invoiceRepository;
|
_InvoiceRepository = invoiceRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ namespace BTCPayServer.Controllers
|
|||||||
})).FirstOrDefault();
|
})).FirstOrDefault();
|
||||||
if (invoice == null)
|
if (invoice == null)
|
||||||
throw new BitpayHttpException(404, "Object not found");
|
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]
|
[HttpGet]
|
||||||
[Route("invoices")]
|
[Route("invoices")]
|
||||||
@@ -93,7 +97,7 @@ namespace BTCPayServer.Controllers
|
|||||||
};
|
};
|
||||||
|
|
||||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
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));
|
return Json(DataWrapper.Create(entities));
|
||||||
}
|
}
|
||||||
@@ -103,7 +107,7 @@ namespace BTCPayServer.Controllers
|
|||||||
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
|
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
|
||||||
{
|
{
|
||||||
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator);
|
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" };
|
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
public async Task<IActionResult> GetNotificationSettings()
|
public async Task<IActionResult> GetNotificationSettings()
|
||||||
{
|
{
|
||||||
var user = await _userManager.GetUserAsync(User);
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user is null)
|
||||||
|
return NotFound();
|
||||||
var model = GetNotificationSettingsData(user);
|
var model = GetNotificationSettingsData(user);
|
||||||
return Ok(model);
|
return Ok(model);
|
||||||
}
|
}
|
||||||
@@ -103,6 +105,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
public async Task<IActionResult> UpdateNotificationSettings(UpdateNotificationSettingsRequest request)
|
public async Task<IActionResult> UpdateNotificationSettings(UpdateNotificationSettingsRequest request)
|
||||||
{
|
{
|
||||||
var user = await _userManager.GetUserAsync(User);
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user is null)
|
||||||
|
return NotFound();
|
||||||
if (request.Disabled.Contains("all"))
|
if (request.Disabled.Contains("all"))
|
||||||
{
|
{
|
||||||
user.DisabledNotifications = "all";
|
user.DisabledNotifications = "all";
|
||||||
|
|||||||
@@ -576,10 +576,10 @@ namespace BTCPayServer.Controllers
|
|||||||
PaymentMethodRaw = data,
|
PaymentMethodRaw = data,
|
||||||
PaymentMethodId = paymentMethodId,
|
PaymentMethodId = paymentMethodId,
|
||||||
PaymentMethod = paymentMethodId.ToString(),
|
PaymentMethod = paymentMethodId.ToString(),
|
||||||
TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency),
|
TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency, divisibility: data.Divisibility),
|
||||||
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency) : null,
|
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency, divisibility: data.Divisibility) : null,
|
||||||
Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency) : null,
|
Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency, divisibility: data.Divisibility) : null,
|
||||||
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency) : null,
|
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency, divisibility: data.Divisibility) : null,
|
||||||
Address = data.Destination
|
Address = data.Destination
|
||||||
};
|
};
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
@@ -876,6 +876,13 @@ namespace BTCPayServer.Controllers
|
|||||||
_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension);
|
_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension);
|
||||||
return extension?.Image ?? "";
|
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
|
var model = new PaymentModel
|
||||||
{
|
{
|
||||||
Activated = prompt.Activated,
|
Activated = prompt.Activated,
|
||||||
@@ -893,10 +900,11 @@ namespace BTCPayServer.Controllers
|
|||||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||||
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
|
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
|
||||||
BtcAddress = prompt.Destination,
|
BtcAddress = prompt.Destination,
|
||||||
BtcDue = accounting.ShowMoney(accounting.Due),
|
BtcDue = ShowMoney(accounting.Due),
|
||||||
BtcPaid = accounting.ShowMoney(accounting.Paid),
|
BtcPaid = ShowMoney(accounting.Paid),
|
||||||
InvoiceCurrency = invoice.Currency,
|
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(),
|
IsUnsetTopUp = invoice.IsUnsetTopUp(),
|
||||||
CustomerEmail = invoice.Metadata.BuyerEmail,
|
CustomerEmail = invoice.Metadata.BuyerEmail,
|
||||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
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,
|
ReceivedConfirmations = handler is BitcoinLikePaymentHandler bh ? invoice.GetAllBitcoinPaymentData(bh, false).FirstOrDefault()?.ConfirmationCount : null,
|
||||||
Status = invoice.Status.ToString(),
|
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,
|
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.PaymentMethodId).Concat(new[] { prompt.PaymentMethodId }).Distinct().Count() > 1,
|
||||||
StoreId = store.Id,
|
StoreId = store.Id,
|
||||||
AvailableCryptos = invoice.GetPaymentPrompts()
|
AvailableCryptos = invoice.GetPaymentPrompts()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
@@ -45,6 +46,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||||
private readonly StoreRepository _StoreRepository;
|
private readonly StoreRepository _StoreRepository;
|
||||||
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
|
||||||
|
private readonly CurrencyNameTable _currencyNameTable;
|
||||||
public const string NamedClient = "bitpay-ipn";
|
public const string NamedClient = "bitpay-ipn";
|
||||||
public BitpayIPNSender(
|
public BitpayIPNSender(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
@@ -53,6 +55,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
InvoiceRepository invoiceRepository,
|
InvoiceRepository invoiceRepository,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
|
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
|
||||||
|
CurrencyNameTable currencyNameTable,
|
||||||
EmailSenderFactory emailSenderFactory)
|
EmailSenderFactory emailSenderFactory)
|
||||||
{
|
{
|
||||||
_Client = httpClientFactory.CreateClient(NamedClient);
|
_Client = httpClientFactory.CreateClient(NamedClient);
|
||||||
@@ -62,11 +65,12 @@ namespace BTCPayServer.HostedServices
|
|||||||
_EmailSenderFactory = emailSenderFactory;
|
_EmailSenderFactory = emailSenderFactory;
|
||||||
_StoreRepository = storeRepository;
|
_StoreRepository = storeRepository;
|
||||||
_bitpayExtensions = bitpayExtensions;
|
_bitpayExtensions = bitpayExtensions;
|
||||||
|
_currencyNameTable = currencyNameTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification, bool sendMail)
|
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()
|
var notification = new InvoicePaymentNotificationEventWrapper()
|
||||||
{
|
{
|
||||||
Data = new InvoicePaymentNotification()
|
Data = new InvoicePaymentNotification()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
|
|
||||||
@@ -13,9 +14,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
public BitcoinPaymentMethodBitpayAPIExtension(
|
public BitcoinPaymentMethodBitpayAPIExtension(
|
||||||
PaymentMethodId paymentMethodId,
|
PaymentMethodId paymentMethodId,
|
||||||
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
|
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
|
||||||
|
CurrencyNameTable currencyNameTable,
|
||||||
PaymentMethodHandlerDictionary handlers)
|
PaymentMethodHandlerDictionary handlers)
|
||||||
{
|
{
|
||||||
PaymentMethodId = paymentMethodId;
|
PaymentMethodId = paymentMethodId;
|
||||||
|
_currencyNameTable = currencyNameTable;
|
||||||
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
|
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
|
||||||
handler = (BitcoinLikePaymentHandler)handlers[paymentMethodId];
|
handler = (BitcoinLikePaymentHandler)handlers[paymentMethodId];
|
||||||
}
|
}
|
||||||
@@ -23,6 +26,16 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
|
|
||||||
private IPaymentLinkExtension paymentLinkExtension;
|
private IPaymentLinkExtension paymentLinkExtension;
|
||||||
private BitcoinLikePaymentHandler handler;
|
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)
|
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),
|
BIP21 = paymentLinkExtension.GetPaymentLink(prompt, urlHelper),
|
||||||
};
|
};
|
||||||
var minerInfo = new MinerFeeInfo();
|
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;
|
minerInfo.SatoshiPerBytes = handler.ParsePaymentPromptDetails(prompt.Details).RecommendedFeeRate.SatoshiPerByte;
|
||||||
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
public IOptions<LightningNetworkOptions> Options { get; }
|
public IOptions<LightningNetworkOptions> Options { get; }
|
||||||
|
|
||||||
public BTCPayNetwork Network => _Network;
|
public BTCPayNetwork Network => _Network;
|
||||||
|
static LightMoney OneSat = LightMoney.FromUnit(1.0m, LightMoneyUnit.Satoshi);
|
||||||
public async Task ConfigurePrompt(PaymentMethodContext context)
|
public async Task ConfigurePrompt(PaymentMethodContext context)
|
||||||
{
|
{
|
||||||
if (context.InvoiceEntity.Type == InvoiceType.TopUp)
|
if (context.InvoiceEntity.Type == InvoiceType.TopUp)
|
||||||
@@ -89,15 +89,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
var nodeInfo = GetNodeInfo(config, context.Logs, preferOnion);
|
var nodeInfo = GetNodeInfo(config, context.Logs, preferOnion);
|
||||||
|
|
||||||
var invoice = context.InvoiceEntity;
|
var invoice = context.InvoiceEntity;
|
||||||
decimal due = Extensions.RoundUp(invoice.Price / paymentPrompt.Rate, paymentPrompt.Divisibility);
|
decimal due = paymentPrompt.Calculate().Due;
|
||||||
try
|
|
||||||
{
|
|
||||||
due = paymentPrompt.Calculate().Due;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
var client = config.CreateLightningClient(_Network, Options.Value, _lightningClientFactory);
|
var client = config.CreateLightningClient(_Network, Options.Value, _lightningClientFactory);
|
||||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||||
if (expiry < TimeSpan.Zero)
|
if (expiry < TimeSpan.Zero)
|
||||||
@@ -116,6 +108,12 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
var request = new CreateInvoiceParams(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
|
var request = new CreateInvoiceParams(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
|
||||||
request.PrivateRouteHints = storeBlob.LightningPrivateRouteHints;
|
request.PrivateRouteHints = storeBlob.LightningPrivateRouteHints;
|
||||||
lightningInvoice = await client.CreateInvoice(request, cts.Token);
|
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)
|
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ public class DisplayFormatter
|
|||||||
/// <param name="currency">Currency code</param>
|
/// <param name="currency">Currency code</param>
|
||||||
/// <param name="format">The format, defaults to amount + code, e.g. 1.234,56 USD</param>
|
/// <param name="format">The format, defaults to amount + code, e.g. 1.234,56 USD</param>
|
||||||
/// <returns>Formatted amount and currency string</returns>
|
/// <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 provider = _currencyNameTable.GetNumberFormatInfo(currency, true);
|
||||||
var currencyData = _currencyNameTable.GetCurrencyData(currency, true);
|
var currencyData = _currencyNameTable.GetCurrencyData(currency, true);
|
||||||
var divisibility = currencyData.Divisibility;
|
var div = divisibility is int d ? d : currencyData.Divisibility;
|
||||||
value = value.RoundToSignificant(ref divisibility);
|
value = value.RoundToSignificant(ref div);
|
||||||
if (divisibility != provider.CurrencyDecimalDigits)
|
if (divisibility != provider.CurrencyDecimalDigits)
|
||||||
{
|
{
|
||||||
provider = (NumberFormatInfo)provider.Clone();
|
provider = (NumberFormatInfo)provider.Clone();
|
||||||
provider.CurrencyDecimalDigits = divisibility;
|
provider.CurrencyDecimalDigits = div;
|
||||||
}
|
}
|
||||||
var formatted = value.ToString("C", provider);
|
var formatted = value.ToString("C", provider);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Payments.Bitcoin;
|
using BTCPayServer.Payments.Bitcoin;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
@@ -521,11 +522,11 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
return DateTimeOffset.UtcNow > ExpirationTime;
|
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 ?? "";
|
ServerUrl = ServerUrl ?? "";
|
||||||
InvoiceResponse dto = new InvoiceResponse
|
InvoiceResponse dto = new InvoiceResponse
|
||||||
@@ -608,8 +609,11 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
// is for legacy compatibility with the Bitpay API
|
// is for legacy compatibility with the Bitpay API
|
||||||
var paymentCode = GetPaymentCode(info.Currency, paymentId);
|
var paymentCode = GetPaymentCode(info.Currency, paymentId);
|
||||||
dto.PaymentCodes.Add(paymentCode, cryptoInfo.PaymentUrls);
|
dto.PaymentCodes.Add(paymentCode, cryptoInfo.PaymentUrls);
|
||||||
dto.PaymentSubtotals.Add(paymentCode, accounting.ToSmallestUnit(subtotalPrice));
|
if (info.Currency is not null && currencyNameTable.GetCurrencyData(info.Currency, true)?.Divisibility is int divisibility)
|
||||||
dto.PaymentTotals.Add(paymentCode, accounting.ToSmallestUnit(accounting.TotalDue));
|
{
|
||||||
|
dto.PaymentSubtotals.Add(paymentCode, BitcoinPaymentMethodBitpayAPIExtension.ToSmallestUnit(divisibility, subtotalPrice));
|
||||||
|
dto.PaymentTotals.Add(paymentCode, BitcoinPaymentMethodBitpayAPIExtension.ToSmallestUnit(divisibility, accounting.TotalDue));
|
||||||
|
}
|
||||||
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
|
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
|
||||||
{
|
{
|
||||||
Enabled = true
|
Enabled = true
|
||||||
@@ -810,7 +814,6 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
|
|
||||||
public class PaymentMethodAccounting
|
public class PaymentMethodAccounting
|
||||||
{
|
{
|
||||||
public int Divisibility { get; set; }
|
|
||||||
/// <summary>Total amount of this invoice</summary>
|
/// <summary>Total amount of this invoice</summary>
|
||||||
public decimal TotalDue { get; set; }
|
public decimal TotalDue { get; set; }
|
||||||
|
|
||||||
@@ -850,23 +853,9 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal PaymentMethodFee { get; set; }
|
public decimal PaymentMethodFee { get; set; }
|
||||||
/// <summary>
|
/// <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
|
/// Minimum required to be paid in order to accept invoice as paid
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal MinimumTotalDue { get; set; }
|
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
|
public class PaymentPrompt
|
||||||
@@ -884,6 +873,26 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
public int Divisibility { get; set; }
|
public int Divisibility { get; set; }
|
||||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
public decimal PaymentMethodFee { get; set; }
|
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 string Destination { get; set; }
|
||||||
public JToken Details { get; set; }
|
public JToken Details { get; set; }
|
||||||
|
|
||||||
@@ -901,7 +910,6 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
accounting.TxRequired++;
|
accounting.TxRequired++;
|
||||||
grossDue += rate * PaymentMethodFee;
|
grossDue += rate * PaymentMethodFee;
|
||||||
}
|
}
|
||||||
accounting.Divisibility = Divisibility;
|
|
||||||
accounting.TotalDue = Coins(grossDue / rate, Divisibility);
|
accounting.TotalDue = Coins(grossDue / rate, Divisibility);
|
||||||
accounting.Paid = Coins(i.PaidAmount.Gross / rate, Divisibility);
|
accounting.Paid = Coins(i.PaidAmount.Gross / rate, Divisibility);
|
||||||
accounting.PaymentMethodPaid = Coins(thisPaymentMethodPayments.Sum(p => p.PaidAmount.Gross), 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.Due = Max(accounting.DueUncapped, 0.0m);
|
||||||
|
|
||||||
accounting.PaymentMethodFee = Coins((grossDue - i.Price) / rate, Divisibility);
|
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));
|
accounting.MinimumTotalDue = Max(Smallest(Divisibility), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / rate, Divisibility));
|
||||||
return accounting;
|
return accounting;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
Type = prettyName.PrettyName(payment.PaymentMethodId),
|
Type = prettyName.PrettyName(payment.PaymentMethodId),
|
||||||
BOLT11 = offChainPaymentData.BOLT11,
|
BOLT11 = offChainPaymentData.BOLT11,
|
||||||
PaymentProof = offChainPaymentData.Preimage?.ToString(),
|
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)
|
.Where(model => model != null)
|
||||||
|
|||||||
Reference in New Issue
Block a user