diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 9787e233f..d74cbd754 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -61,10 +61,9 @@ namespace BTCPayServer.Tests await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); StoreId = store.CreatedStoreId; DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); - await store.UpdateStore(StoreId, new StoreViewModel() - { - SpeedPolicy = SpeedPolicy.MediumSpeed - }); + var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model; + vm.SpeedPolicy = SpeedPolicy.MediumSpeed; + await store.UpdateStore(StoreId, vm); await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() { diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index f7984b94f..25a237386 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -24,6 +24,7 @@ using BTCPayServer.Services.Rates; using Microsoft.Extensions.Caching.Memory; using BTCPayServer.Eclair; using System.Collections.Generic; +using BTCPayServer.Models.StoreViewModels; namespace BTCPayServer.Tests { @@ -392,6 +393,50 @@ namespace BTCPayServer.Tests } } + + [Fact] + public void CanTweakRate() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + + + // First we try payment with a merchant having only BTC + var invoice1 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + + var storeController = tester.PayTester.GetController(user.UserId); + var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; + Assert.Equal(1.0, vm.RateMultiplier); + vm.RateMultiplier = 0.5; + storeController.UpdateStore(user.StoreId, vm).Wait(); + + + var invoice2 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m)); + } + } + [Fact] public void CanHaveLTCOnlyStore() { diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 437db58e8..e1e470734 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -130,7 +130,7 @@ namespace BTCPayServer.Controllers { network = _.Network, getFeeRate = _.FeeRateProvider.GetFeeRateAsync(), - getRate = _.RateProvider.GetRateAsync(invoice.Currency), + getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency), getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy) }; }); @@ -164,7 +164,7 @@ namespace BTCPayServer.Controllers #pragma warning disable CS0618 var btc = _NetworkProvider.BTC; var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc); - var rateProvider = _RateProviders.GetRateProvider(btc); + var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc)); if (feeProvider != null && rateProvider != null) { var gettingFee = feeProvider.GetFeeRateAsync(); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 139294ee3..3730b9993 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -164,6 +164,7 @@ namespace BTCPayServer.Controllers vm.StatusMessage = StatusMessage; vm.MonitoringExpiration = storeBlob.MonitoringExpiration; vm.InvoiceExpiration = storeBlob.InvoiceExpiration; + vm.RateMultiplier = (double)storeBlob.GetRateMultiplier(); return View(vm); } @@ -307,6 +308,8 @@ namespace BTCPayServer.Controllers var blob = store.GetStoreBlob(); blob.NetworkFeeDisabled = !model.NetworkFee; blob.MonitoringExpiration = model.MonitoringExpiration; + blob.InvoiceExpiration = model.InvoiceExpiration; + blob.SetRateMultiplier(model.RateMultiplier); if (store.SetStoreBlob(blob)) { diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index ab0da6c31..ff98ee20a 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.ComponentModel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using BTCPayServer.Services.Rates; namespace BTCPayServer.Data { @@ -99,10 +100,10 @@ namespace BTCPayServer.Data if (!existing && string.IsNullOrEmpty(derivationScheme)) { - if(network.IsBTC) + if (network.IsBTC) DerivationStrategy = null; } - else if(!existing) + else if (!existing) strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme))); // This is deprecated so we don't have to set anymore //if (network.IsBTC) @@ -173,6 +174,22 @@ namespace BTCPayServer.Data } } + public class RateRule + { + public RateRule() + { + RuleName = "Multiplier"; + } + public string RuleName { get; set; } + + public double Multiplier { get; set; } + + public decimal Apply(BTCPayNetwork network, decimal rate) + { + return rate * (decimal)Multiplier; + } + } + public class StoreBlob { public StoreBlob() @@ -200,5 +217,30 @@ namespace BTCPayServer.Data set; } + public void SetRateMultiplier(double rate) + { + RateRules = new List(); + RateRules.Add(new RateRule() { Multiplier = rate }); + } + public decimal GetRateMultiplier() + { + decimal rate = 1.0m; + if (RateRules == null || RateRules.Count == 0) + return rate; + foreach (var rule in RateRules) + { + rate = rule.Apply(null, rate); + } + return rate; + } + + public List RateRules { get; set; } = new List(); + + public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider) + { + if (RateRules == null || RateRules.Count == 0) + return rateProvider; + return new TweakRateProvider(network, rateProvider, RateRules.ToList()); + } } } diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index 1c1155cf7..a6fb4abb3 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -47,6 +47,14 @@ namespace BTCPayServer.Models.StoreViewModels public List DerivationSchemes { get; set; } = new List(); + [Display(Name = "Multiply the original rate by ...")] + [Range(0.01, 10.0)] + public double RateMultiplier + { + get; + set; + } + [Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")] [Range(1, 60 * 24 * 31)] public int InvoiceExpiration diff --git a/BTCPayServer/Services/Rates/TweakRateProvider.cs b/BTCPayServer/Services/Rates/TweakRateProvider.cs new file mode 100644 index 000000000..292f9787f --- /dev/null +++ b/BTCPayServer/Services/Rates/TweakRateProvider.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; + +namespace BTCPayServer.Services.Rates +{ + public class TweakRateProvider : IRateProvider + { + private BTCPayNetwork network; + private IRateProvider rateProvider; + private List rateRules; + + public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List rateRules) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + if (rateProvider == null) + throw new ArgumentNullException(nameof(rateProvider)); + if (rateRules == null) + throw new ArgumentNullException(nameof(rateRules)); + this.network = network; + this.rateProvider = rateProvider; + this.rateRules = rateRules; + } + + public async Task GetRateAsync(string currency) + { + var rate = await rateProvider.GetRateAsync(currency); + foreach(var rule in rateRules) + { + rate = rule.Apply(network, rate); + } + return rate; + } + + public async Task> GetRatesAsync() + { + List rates = new List(); + foreach (var rate in await rateProvider.GetRatesAsync()) + { + var localRate = rate.Value; + foreach (var rule in rateRules) + { + localRate = rule.Apply(network, localRate); + } + rates.Add(new Rate(rate.Currency, localRate)); + } + return rates; + } + } +} diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index 2cac15730..9c607b078 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -38,6 +38,11 @@ +
+ + + +