mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 06:35:13 +01:00
Merge pull request #489 from btcpayserver/feature/networkfee
Add support for removing network fee on first payment
This commit is contained in:
commit
ae9ad0fa65
18 changed files with 199 additions and 44 deletions
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue