Merge pull request #489 from btcpayserver/feature/networkfee

Add support for removing network fee on first payment
This commit is contained in:
Nicolas Dorier 2019-01-05 22:09:10 +09:00 committed by GitHub
commit ae9ad0fa65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 199 additions and 44 deletions

View file

@ -34,6 +34,7 @@ using System.Security.Principal;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Xunit; using Xunit;
using BTCPayServer.Services;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {

View file

@ -22,6 +22,7 @@ using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning; using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Services;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@ -152,6 +153,7 @@ namespace BTCPayServer.Tests
{ {
get; set; get; set;
} }
public List<string> Stores { get; internal set; } = new List<string>(); public List<string> Stores { get; internal set; } = new List<string>();
public void Dispose() public void Dispose()

View file

@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning; using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@ -58,6 +59,14 @@ namespace BTCPayServer.Tests
CreateStoreAsync().GetAwaiter().GetResult(); CreateStoreAsync().GetAwaiter().GetResult();
} }
public void SetNetworkFeeMode(NetworkFeeMode mode)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
store.NetworkFeeMode = mode;
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller public T GetController<T>(bool setImplicitStore = true) where T : Controller
{ {
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null); return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);

View file

@ -49,6 +49,8 @@ using BTCPayServer.Security;
using NBXplorer.Models; using NBXplorer.Models;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel; using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
using NBitpayClient.Extensions; using NBitpayClient.Extensions;
using BTCPayServer.Services;
using System.Text.RegularExpressions;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@ -349,7 +351,7 @@ namespace BTCPayServer.Tests
(1000.0001m, "₹ 1,000.00 (INR)", "INR") (1000.0001m, "₹ 1,000.00 (INR)", "INR")
}) })
{ {
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3); var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual); Assert.Equal(test.Item2, actual);
} }
@ -494,6 +496,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.GrantAccess(); acc.GrantAccess();
acc.RegisterDerivationScheme("BTC"); acc.RegisterDerivationScheme("BTC");
acc.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = acc.BitPay.CreateInvoice(new Invoice() var invoice = acc.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5.0m, Price = 5.0m,
@ -726,6 +729,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5000.0m, Price = 5000.0m,
@ -1591,13 +1595,19 @@ donation:
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public void CanExportInvoicesJson() public void CanExportInvoicesJson()
{ {
decimal GetFieldValue(string input, string fieldName)
{
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
Assert.True(match.Success);
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
}
using (var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 10, Price = 10,
@ -1608,8 +1618,7 @@ donation:
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
var networkFee = Money.Satoshis(10000); var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
// ensure 0 invoices exported because there are no payments yet // ensure 0 invoices exported because there are no payments yet
var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult(); var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
var result = Assert.IsType<ContentResult>(jsonResult); var result = Assert.IsType<ContentResult>(jsonResult);
@ -1619,7 +1628,7 @@ donation:
var cashCow = tester.ExplorerNode; var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
// //
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3*networkFee; var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment); cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments Thread.Sleep(1000); // prevent race conditions, ordering payments
// look if you can reduce thread sleep, this was min value for me // look if you can reduce thread sleep, this was min value for me
@ -1629,7 +1638,7 @@ donation:
Thread.Sleep(1000); Thread.Sleep(1000);
// pay remaining amount // pay remaining amount
cashCow.SendToAddress(invoiceAddress, 4*networkFee); cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000); Thread.Sleep(1000);
Eventually(() => Eventually(() =>
@ -1641,21 +1650,102 @@ donation:
var parsedJson = JsonConvert.DeserializeObject<object[]>(paidresult.Content); var parsedJson = JsonConvert.DeserializeObject<object[]>(paidresult.Content);
Assert.Equal(3, parsedJson.Length); Assert.Equal(3, parsedJson.Length);
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var pay1str = parsedJson[0].ToString(); var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Contains("\"InvoiceDue\": 1.5", pay1str); Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
Assert.Contains("\"InvoicePrice\": 10.0", pay1str); Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
Assert.Contains("\"ConversionRate\": 5000.0", pay1str); Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str); Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
var pay2str = parsedJson[1].ToString(); var pay2str = parsedJson[1].ToString();
Assert.Contains("\"InvoiceDue\": 1.5", pay2str); Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
var pay3str = parsedJson[2].ToString(); var pay3str = parsedJson[2].ToString();
Assert.Contains("\"InvoiceDue\": 0", pay3str); Assert.Contains("\"InvoiceDue\": 0", pay3str);
}); });
} }
} }
[Fact]
[Trait("Integration", "Integration")]
public void CanChangeNetworkFeeMode()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
{
Logs.Tester.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}");
user.SetNetworkFeeMode(networkFeeMode);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var networkFee = Money.Satoshis(10000).ToDecimal(MoneyUnit.BTC);
var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
// Check that for the first payment, no network fee are included
var due = Money.Parse(invoice.CryptoInfo[0].Due);
var productPartDue = (invoice.Price / invoice.Rate);
switch (networkFeeMode)
{
case NetworkFeeMode.MultiplePaymentsOnly:
case NetworkFeeMode.Never:
Assert.Equal(productPartDue, due.ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Always:
Assert.Equal(productPartDue + networkFee, due.ToDecimal(MoneyUnit.BTC));
break;
default:
throw new NotSupportedException(networkFeeMode.ToString());
}
var firstPayment = productPartDue - missingMoney;
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
// Check that for the second payment, network fee are included
due = Money.Parse(invoice.CryptoInfo[0].Due);
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
switch (networkFeeMode)
{
case NetworkFeeMode.MultiplePaymentsOnly:
Assert.Equal(missingMoney + networkFee, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney + networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Always:
Assert.Equal(missingMoney + 2 * networkFee, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney + 2 * networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Never:
Assert.Equal(missingMoney, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
default:
throw new NotSupportedException(networkFeeMode.ToString());
}
});
cashCow.SendToAddress(invoiceAddress, due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
}
}
}
[Fact] [Fact]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
@ -1667,7 +1757,7 @@ donation:
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 500, Price = 500,

View file

@ -200,8 +200,6 @@ namespace BTCPayServer.Controllers
paymentMethod.SetId(supportedPaymentMethod.PaymentId); paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid; paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment); var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoNetworkFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails); paymentMethod.SetPaymentMethodDetails(paymentDetails);
Func<Money, Money, bool> compare = null; Func<Money, Money, bool> compare = null;

View file

@ -406,7 +406,7 @@ namespace BTCPayServer.Controllers
vm.Id = store.Id; vm.Id = store.Id;
vm.StoreName = store.StoreName; vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite; vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled; vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice; vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy; vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores(); vm.CanDelete = _Repo.CanDeleteStores();
@ -489,7 +489,7 @@ namespace BTCPayServer.Controllers
var blob = StoreData.GetStoreBlob(); var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeDisabled = !model.NetworkFee; blob.NetworkFeeMode = model.NetworkFeeMode;
blob.MonitoringExpiration = model.MonitoringExpiration; blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration; blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty; blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;

View file

@ -249,6 +249,12 @@ namespace BTCPayServer.Data
} }
} }
public enum NetworkFeeMode
{
MultiplePaymentsOnly,
Always,
Never
}
public class StoreBlob public class StoreBlob
{ {
public StoreBlob() public StoreBlob()
@ -258,10 +264,21 @@ namespace BTCPayServer.Data
PaymentTolerance = 0; PaymentTolerance = 0;
RequiresRefundEmail = true; RequiresRefundEmail = true;
} }
public bool NetworkFeeDisabled
[Obsolete("Use NetworkFeeMode instead")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? NetworkFeeDisabled
{ {
get; set; get; set;
} }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public NetworkFeeMode NetworkFeeMode
{
get;
set;
}
public bool RequiresRefundEmail { get; set; } public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; } public string DefaultLang { get; set; }

View file

@ -59,6 +59,12 @@ namespace BTCPayServer.HostedServices
settings.ConvertMultiplierToSpread = true; settings.ConvertMultiplierToSpread = true;
await _Settings.UpdateSetting(settings); await _Settings.UpdateSetting(settings);
} }
if (!settings.ConvertNetworkFeeProperty)
{
await ConvertNetworkFeeProperty();
settings.ConvertNetworkFeeProperty = true;
await _Settings.UpdateSetting(settings);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -67,6 +73,26 @@ namespace BTCPayServer.HostedServices
} }
} }
private async Task ConvertNetworkFeeProperty()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
var blob = store.GetStoreBlob();
#pragma warning disable CS0618 // Type or member is obsolete
if (blob.NetworkFeeDisabled != null)
{
blob.NetworkFeeMode = blob.NetworkFeeDisabled.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always;
blob.NetworkFeeDisabled = null;
store.SetStoreBlob(blob);
}
#pragma warning restore CS0618 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
}
private async Task ConvertMultiplierToSpread() private async Task ConvertMultiplierToSpread()
{ {
using (var ctx = _DBContextFactory.CreateContext()) using (var ctx = _DBContextFactory.CreateContext())

View file

@ -82,8 +82,8 @@ namespace BTCPayServer.Models.StoreViewModels
get; set; get; set;
} }
[Display(Name = "Add network fee to invoice (vary with mining fees)")] [Display(Name = "Add additional fee (network fee) to invoice...")]
public bool NetworkFee public Data.NetworkFeeMode NetworkFeeMode
{ {
get; set; get; set;
} }

View file

@ -24,17 +24,11 @@ namespace BTCPayServer.Payments.Bitcoin
{ {
return NetworkFee.ToDecimal(MoneyUnit.BTC); return NetworkFee.ToDecimal(MoneyUnit.BTC);
} }
public void SetNoNetworkFee()
{
NetworkFee = Money.Zero;
}
public void SetPaymentDestination(string newPaymentDestination) public void SetPaymentDestination(string newPaymentDestination)
{ {
DepositAddress = newPaymentDestination; DepositAddress = newPaymentDestination;
} }
public Data.NetworkFeeMode NetworkFeeMode { get; set; }
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason // Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
[JsonIgnore] [JsonIgnore]

View file

@ -48,8 +48,18 @@ namespace BTCPayServer.Payments.Bitcoin
throw new PaymentMethodUnavailableException($"Full node not available"); throw new PaymentMethodUnavailableException($"Full node not available");
var prepare = (Prepare)preparePaymentObject; var prepare = (Prepare)preparePaymentObject;
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod(); Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.NetworkFeeMode = store.GetStoreBlob().NetworkFeeMode;
onchainMethod.FeeRate = await prepare.GetFeeRate; onchainMethod.FeeRate = await prepare.GetFeeRate;
onchainMethod.NetworkFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes switch (onchainMethod.NetworkFeeMode)
{
case NetworkFeeMode.Always:
onchainMethod.NetworkFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
break;
case NetworkFeeMode.Never:
case NetworkFeeMode.MultiplePaymentsOnly:
onchainMethod.NetworkFee = Money.Zero;
break;
}
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString(); onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
return onchainMethod; return onchainMethod;
} }

View file

@ -158,7 +158,7 @@ namespace BTCPayServer.Payments.Bitcoin
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist) if (!alreadyExist)
{ {
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode); var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
if(payment != null) if(payment != null)
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
} }
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
{ {
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash); var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF); var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false); var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint); alreadyAccounted.Add(coin.Coin.Outpoint);
if (payment != null) if (payment != null)
{ {

View file

@ -22,7 +22,6 @@ namespace BTCPayServer.Payments
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
decimal GetNetworkFee(); decimal GetNetworkFee();
void SetNoNetworkFee();
/// <summary> /// <summary>
/// Change the payment destination (internal plumbing) /// Change the payment destination (internal plumbing)
/// </summary> /// </summary>

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning namespace BTCPayServer.Payments.Lightning
{ {
@ -25,11 +26,6 @@ namespace BTCPayServer.Payments.Lightning
{ {
return 0.0m; return 0.0m;
} }
public void SetNoNetworkFee()
{
}
public void SetPaymentDestination(string newPaymentDestination) public void SetPaymentDestination(string newPaymentDestination)
{ {
BOLT11 = newPaymentDestination; BOLT11 = newPaymentDestination;

View file

@ -192,7 +192,7 @@ namespace BTCPayServer.Payments.Lightning
{ {
BOLT11 = notification.BOLT11, BOLT11 = notification.BOLT11,
Amount = notification.Amount Amount = notification.Amount
}, network.CryptoCode, accounted: true); }, network, accounted: true);
if (payment != null) if (payment != null)
{ {
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId); var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);

View file

@ -193,7 +193,7 @@ retry:
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
} }
public async Task<bool> NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetwork network) public async Task<bool> NewAddress(string invoiceId, Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod paymentMethod, BTCPayNetwork network)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
@ -206,14 +206,13 @@ retry:
if (currencyData == null) if (currencyData == null)
return false; return false;
var existingPaymentMethod = currencyData.GetPaymentMethodDetails(); var existingPaymentMethod = (Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)currencyData.GetPaymentMethodDetails();
if (existingPaymentMethod.GetPaymentDestination() != null) if (existingPaymentMethod.GetPaymentDestination() != null)
{ {
MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId()); MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId());
} }
existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination()); existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination());
currencyData.SetPaymentMethodDetails(existingPaymentMethod); currencyData.SetPaymentMethodDetails(existingPaymentMethod);
#pragma warning disable CS0618 #pragma warning disable CS0618
if (network.IsBTC) if (network.IsBTC)
@ -560,28 +559,37 @@ retry:
/// <param name="cryptoCode"></param> /// <param name="cryptoCode"></param>
/// <param name="accounted"></param> /// <param name="accounted"></param>
/// <returns>The PaymentEntity or null if already added</returns> /// <returns>The PaymentEntity or null if already added</returns>
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false) public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetwork network, bool accounted = false)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
var invoice = context.Invoices.Find(invoiceId); var invoice = context.Invoices.Find(invoiceId);
if (invoice == null) if (invoice == null)
return null; return null;
InvoiceEntity invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob, network.NBitcoinNetwork);
PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()), null);
IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
PaymentEntity entity = new PaymentEntity PaymentEntity entity = new PaymentEntity
{ {
Version = 1, Version = 1,
#pragma warning disable CS0618 #pragma warning disable CS0618
CryptoCode = cryptoCode, CryptoCode = network.CryptoCode,
#pragma warning restore CS0618 #pragma warning restore CS0618
ReceivedTime = date.UtcDateTime, ReceivedTime = date.UtcDateTime,
Accounted = accounted, Accounted = accounted,
NetworkFee = ToObject<InvoiceEntity>(invoice.Blob, null) NetworkFee = paymentMethodDetails.GetNetworkFee()
.GetPaymentMethod(new PaymentMethodId(cryptoCode, paymentData.GetPaymentType()), null)
.GetPaymentMethodDetails().GetNetworkFee()
}; };
entity.SetCryptoPaymentData(paymentData); entity.SetCryptoPaymentData(paymentData);
if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod &&
bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly &&
bitcoinPaymentMethod.NetworkFee == Money.Zero)
{
bitcoinPaymentMethod.NetworkFee = bitcoinPaymentMethod.FeeRate.GetFee(100); // assume price for 100 bytes
paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod);
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
}
PaymentData data = new PaymentData PaymentData data = new PaymentData
{ {
Id = paymentData.GetPaymentId(), Id = paymentData.GetPaymentId(),

View file

@ -10,5 +10,6 @@ namespace BTCPayServer.Services
public bool UnreachableStoreCheck { get; set; } public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; } public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; } public bool ConvertMultiplierToSpread { get; set; }
public bool ConvertNetworkFeeProperty { get; set; }
} }
} }

View file

@ -43,9 +43,13 @@
<span asp-validation-for="StoreWebsite" class="text-danger"></span> <span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="NetworkFee"></label> <label asp-for="NetworkFeeMode"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a> <a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="NetworkFee" type="checkbox" class="form-check" /> <select asp-for="NetworkFeeMode" class="form-control">
<option value="MultiplePaymentsOnly">... only if the customer makes more than one payment for the invoice</option>
<option value="Always">... on every payment</option>
<option value="Never">Never add network fee</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="AnyoneCanCreateInvoice"></label> <label asp-for="AnyoneCanCreateInvoice"></label>