diff --git a/BTCPayServer.Tests/ChangellyTests.cs b/BTCPayServer.Tests/ChangellyTests.cs new file mode 100644 index 000000000..9ffc38f76 --- /dev/null +++ b/BTCPayServer.Tests/ChangellyTests.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Controllers; +using BTCPayServer.Models; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.Changelly; +using BTCPayServer.Services.Stores; +using BTCPayServer.Tests.Logging; +using Changelly.ResponseModel; +using Microsoft.AspNetCore.Mvc; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests +{ + public class ChangellyTests + { + public ChangellyTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact] + public async void CanSetChangellyPaymentMethod() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + + var storeBlob = controller.StoreData.GetStoreBlob(); + Assert.Null(storeBlob.ChangellySettings); + + var updateModel = new UpdateChangellySettingsViewModel() + { + ApiSecret = "secret", + ApiKey = "key", + ApiUrl = "url", + ChangellyMerchantId = "aaa", + }; + + Assert.Equal("UpdateStore", Assert.IsType( + await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); + storeBlob = controller.StoreData.GetStoreBlob(); + Assert.NotNull(storeBlob.ChangellySettings); + Assert.NotNull(storeBlob.ChangellySettings); + Assert.IsType(storeBlob.ChangellySettings); + Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey); + Assert.Equal(storeBlob.ChangellySettings.ApiSecret, + updateModel.ApiSecret); + Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl); + Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId, + updateModel.ChangellyMerchantId); + } + } + + + [Fact] + public async void CanToggleChangellyPaymentMethod() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + var updateModel = new UpdateChangellySettingsViewModel() + { + ApiSecret = "secret", + ApiKey = "key", + ApiUrl = "url", + ChangellyMerchantId = "aaa", + }; + Assert.Equal("UpdateStore", Assert.IsType( + await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + + var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); + + Assert.True(store.GetStoreBlob().ChangellySettings.Enabled); + + updateModel.Enabled = false; + + Assert.Equal("UpdateStore", Assert.IsType( + await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); + + Assert.False(store.GetStoreBlob().ChangellySettings.Enabled); + } + } + + [Fact] + public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var changellyController = + tester.PayTester.GetController(user.UserId, user.StoreId); + + //test non existing payment method + Assert.IsType(Assert + .IsType(await changellyController.GetCurrencyList(user.StoreId)) + .Value); + + var updateModel = new UpdateChangellySettingsViewModel() + { + ApiSecret = "secret", + ApiKey = "key", + ApiUrl = "url", + ChangellyMerchantId = "aaa", + Enabled = false + }; + var storesController = tester.PayTester.GetController(user.UserId, user.StoreId); + //set payment method but disabled + + + Assert.Equal("UpdateStore", Assert.IsType( + await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + + Assert.IsType(Assert + .IsType(await changellyController.GetCurrencyList(user.StoreId)) + .Value); + + updateModel.Enabled = true; + //test with enabled method + + Assert.Equal("UpdateStore", Assert.IsType( + await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + Assert.IsNotType(Assert + .IsType(await changellyController.GetCurrencyList(user.StoreId)) + .Value); + } + } + + [Fact] + public async void CanGetCurrencyListFromChangelly() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + + var updateModel = new UpdateChangellySettingsViewModel() + { + ApiSecret = "secret", + ApiKey = "key", + ApiUrl = "url", + ChangellyMerchantId = "aaa" + }; + var storesController = tester.PayTester.GetController(user.UserId, user.StoreId); + + + Assert.Equal("UpdateStore", Assert.IsType( + await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + + var mock = new MockChangellyClientProvider(tester.PayTester.StoreRepository, tester.NetworkProvider); + var changellyController = new ChangellyController(mock); + + mock.GetCurrenciesFullResult = (new List() + { + new CurrencyFull() + { + Name = "a", + Enable = true, + PayInConfirmations = 10, + FullName = "aa", + ImageLink = "" + } + }, true, ""); + var result = ((IList currency, bool Success, string Error))Assert + .IsType(await changellyController.GetCurrencyList(user.StoreId)).Value; + Assert.Equal(1, mock.GetCurrenciesFullCallCount); + Assert.Equal(mock.GetCurrenciesFullResult.currency.Count, result.currency.Count); + + mock.GetCurrenciesFullResult = (new List() + { + new CurrencyFull() + { + Name = "a", + Enable = true, + PayInConfirmations = 10, + FullName = "aa", + ImageLink = "" + } + }, false, ""); + Assert + .IsType(await changellyController.GetCurrencyList(user.StoreId)); + Assert.Equal(2, mock.GetCurrenciesFullCallCount); + } + } + + + [Fact] + public async void CanCalculateToAmountForChangelly() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + + var updateModel = new UpdateChangellySettingsViewModel() + { + ApiSecret = "secret", + ApiKey = "key", + ApiUrl = "url", + ChangellyMerchantId = "aaa" + }; + var storesController = tester.PayTester.GetController(user.UserId, user.StoreId); + + Assert.Equal("UpdateStore", Assert.IsType( + await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName); + + var mock = new MockChangellyClientProvider(tester.PayTester.StoreRepository, tester.NetworkProvider); + var changellyController = new ChangellyController(mock); + + mock.GetExchangeAmountResult = (from, to, amount) => + { + Assert.Equal("A", from); + Assert.Equal("B", to); + + switch (mock.GetExchangeAmountCallCount) + { + case 1: + return (0.5, true, null); + break; + default: + return (1.01, true, null); + break; + } + }; + + Assert.IsType(Assert + .IsType(changellyController.CalculateAmount(user.StoreId, "A", "B", 1.0)).Value); + Assert.True(mock.GetExchangeAmountCallCount > 1); + } + } + } + + public class MockChangellyClientProvider : ChangellyClientProvider + { + public MockChangellyClientProvider( + StoreRepository storeRepository, + BTCPayNetworkProvider btcPayNetworkProvider) : base(storeRepository, btcPayNetworkProvider) + { + } + + public (IList currency, bool Success, string Error) GetCurrenciesFullResult { get; set; } + + public delegate TResult ParamsFunc(T1 arg1, T2 arg2, T3 arg3); + + public ParamsFunc GetExchangeAmountResult + { + get; + set; + } + + public int GetCurrenciesFullCallCount { get; set; } = 0; + public int GetExchangeAmountCallCount { get; set; } = 0; + + public override (IList currency, bool Success, string Error) GetCurrenciesFull( + Changelly.Changelly client) + { + GetCurrenciesFullCallCount++; + return GetCurrenciesFullResult; + } + + public override (double amount, bool Success, string Error) GetExchangeAmount(Changelly.Changelly client, + string fromCurrency, string toCurrency, + double amount) + { + GetExchangeAmountCallCount++; + return GetExchangeAmountResult.Invoke(fromCurrency, toCurrency, amount); + } + } +} diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 3cc8fdc53..581532a83 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -35,6 +35,7 @@ + diff --git a/BTCPayServer/Controllers/ChangellyController.cs b/BTCPayServer/Controllers/ChangellyController.cs new file mode 100644 index 000000000..05e6c74f6 --- /dev/null +++ b/BTCPayServer/Controllers/ChangellyController.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Models; +using BTCPayServer.Payments.Changelly; +using Changelly.ResponseModel; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + [Route("[controller]/{storeId}")] + public class ChangellyController : Controller + { + private readonly ChangellyClientProvider _changellyClientProvider; + + public ChangellyController(ChangellyClientProvider changellyClientProvider) + { + _changellyClientProvider = changellyClientProvider; + } + + [HttpGet] + [Route("currencies")] + public async Task GetCurrencyList(string storeId) + { + if (!TryGetChangellyClient(storeId, out var actionResult, out var client)) + { + return actionResult; + } + + var result = _changellyClientProvider.GetCurrenciesFull(client); + if (result.Success) + { + return Ok(result); + } + + return BadRequest(result); + } + + [HttpGet] + [Route("calculate")] + public IActionResult CalculateAmount(string storeId, string fromCurrency, string toCurrency, + double toCurrencyAmount) + { + if (!TryGetChangellyClient(storeId, out var actionResult, out var client)) + { + return actionResult; + } + + double? currentAmount = null; + var callCounter = 0; + + var response1 = _changellyClientProvider.GetExchangeAmount(client,fromCurrency, toCurrency, 1); + if (!response1.Success) return BadRequest(response1); + currentAmount = response1.amount; + + while (true) + { + if (callCounter > 10) + { + BadRequest(); + } + + //Client needs to be reset between same calls for some reason + if (!TryGetChangellyClient(storeId, out actionResult, out client)) + { + return actionResult; + } + + var response2 = _changellyClientProvider.GetExchangeAmount(client,fromCurrency, toCurrency, currentAmount.Value); + callCounter++; + if (!response2.Success) return BadRequest(response2); + if (response2.amount < toCurrencyAmount) + { + var newCurrentAmount = ((toCurrencyAmount / response2.amount) * 1) * currentAmount.Value; + currentAmount = newCurrentAmount; + } + else + { + return Ok(currentAmount.Value); + } + } + } + + private bool TryGetChangellyClient(string storeId, out IActionResult actionResult, + out Changelly.Changelly changelly) + { + changelly = null; + actionResult = null; + storeId = storeId ?? HttpContext.GetStoreData()?.Id; + + if (!_changellyClientProvider.TryGetChangellyClient(storeId, out var error, out changelly)) + { + actionResult = BadRequest(new BitpayErrorModel() + { + Error = error + }); + return false; + } + + return true; + } + } +} diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 1987b5ea0..aa266603e 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -10,6 +10,7 @@ using BTCPayServer.Events; using BTCPayServer.Filters; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; +using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Lightning; using BTCPayServer.Security; using BTCPayServer.Services.Invoices; @@ -213,7 +214,6 @@ namespace BTCPayServer.Controllers paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider); isDefaultCrypto = true; } - var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr); var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); if (network == null && isDefaultCrypto) @@ -245,6 +245,18 @@ namespace BTCPayServer.Controllers var storeBlob = store.GetStoreBlob(); var currency = invoice.ProductInformation.Currency; var accounting = paymentMethod.Calculate(); + + ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled && + storeBlob.ChangellySettings.IsConfigured()) + ? storeBlob.ChangellySettings + : null; + + + var changellyAmountDue = changelly != null + ? (accounting.Due.ToDecimal(MoneyUnit.BTC) * + (1m + (changelly.AmountMarkupPercentage / 100m))) + : (decimal?)null; + var model = new PaymentModel() { CryptoCode = network.CryptoCode, @@ -284,7 +296,10 @@ namespace BTCPayServer.Controllers Status = invoice.Status, NetworkFee = paymentMethodDetails.GetTxFee(), IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1, - AllowCoinConversion = storeBlob.AllowCoinConversion, + ChangellyEnabled = changelly != null, + ChangellyMerchantId = changelly?.ChangellyMerchantId, + ChangellyAmountDue = changellyAmountDue, + StoreId = store.Id, AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider) .Where(i => i.Network != null) .Select(kv => new PaymentModel.AvailableCrypto() diff --git a/BTCPayServer/Controllers/StoresController.Changelly.cs b/BTCPayServer/Controllers/StoresController.Changelly.cs new file mode 100644 index 000000000..0b234d1d1 --- /dev/null +++ b/BTCPayServer/Controllers/StoresController.Changelly.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.Changelly; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class StoresController + { + [HttpGet] + [Route("{storeId}/changelly")] + public IActionResult UpdateChangellySettings(string storeId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel(); + SetExistingValues(store, vm); + return View(vm); + } + + private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm) + { + + var existing = store.GetStoreBlob().ChangellySettings; + if (existing == null) return; + vm.ApiKey = existing.ApiKey; + vm.ApiSecret = existing.ApiSecret; + vm.ApiUrl = existing.ApiUrl; + vm.ChangellyMerchantId = existing.ChangellyMerchantId; + vm.Enabled = existing.Enabled; + vm.AmountMarkupPercentage = existing.AmountMarkupPercentage; + + } + + [HttpPost] + [Route("{storeId}/changelly")] + public async Task UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm, + string command) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + if (vm.Enabled) + { + if (!ModelState.IsValid) + { + return View(vm); + } + } + + var changellySettings = new ChangellySettings() + { + ApiKey = vm.ApiKey, + ApiSecret = vm.ApiSecret, + ApiUrl = vm.ApiUrl, + ChangellyMerchantId = vm.ChangellyMerchantId, + Enabled = vm.Enabled, + AmountMarkupPercentage = vm.AmountMarkupPercentage + }; + + switch (command) + { + case "save": + var storeBlob = store.GetStoreBlob(); + storeBlob.ChangellySettings = changellySettings; + store.SetStoreBlob(storeBlob); + await _Repo.UpdateStore(store); + StatusMessage = "Changelly settings modified"; + return RedirectToAction(nameof(UpdateStore), new { + storeId}); + case "test": + try + { + var client = new Changelly.Changelly(changellySettings.ApiKey, changellySettings.ApiSecret, + changellySettings.ApiUrl); + var result = client.GetCurrenciesFull(); + vm.StatusMessage = !result.Success + ? $"Error: {result.Error}" + : "Test Successful"; + return View(vm); + } + catch (Exception ex) + { + vm.StatusMessage = $"Error: {ex.Message}"; + return View(vm); + } + + break; + default: + return View(vm); + } + } + } +} diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index a50ad4b21..0fe7515e3 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -9,6 +9,7 @@ using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.Changelly; using BTCPayServer.Rating; using BTCPayServer.Security; using BTCPayServer.Services; @@ -318,7 +319,6 @@ namespace BTCPayServer.Controllers vm.SetLanguages(_LangService, storeBlob.DefaultLang); vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? ""; vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? ""; - vm.AllowCoinConversion = storeBlob.AllowCoinConversion; vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri; vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri; @@ -362,7 +362,6 @@ namespace BTCPayServer.Controllers return View(model); } blob.DefaultLang = model.DefaultLang; - blob.AllowCoinConversion = model.AllowCoinConversion; blob.RequiresRefundEmail = model.RequiresRefundEmail; blob.LightningMaxValue = lightningMaxValue; blob.OnChainMinValue = onchainMinValue; @@ -447,6 +446,15 @@ namespace BTCPayServer.Controllers Enabled = !excludeFilters.Match(paymentId) }); } + + + var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled; + vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod() + { + Enabled = changellyEnabled, + Action = nameof(UpdateChangellySettings), + Provider = "Changelly" + }); } [HttpPost] diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 19fcdfc25..ab2955047 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -17,6 +17,7 @@ using BTCPayServer.JsonConverters; using System.ComponentModel.DataAnnotations; using BTCPayServer.Services; using System.Security.Claims; +using BTCPayServer.Payments.Changelly; using BTCPayServer.Security; using BTCPayServer.Rating; @@ -261,11 +262,6 @@ namespace BTCPayServer.Data { get; set; } - public bool AllowCoinConversion - { - get; set; - } - public bool RequiresRefundEmail { get; set; } public string DefaultLang { get; set; } @@ -307,6 +303,8 @@ namespace BTCPayServer.Data public string RateScript { get; set; } public bool AnyoneCanInvoice { get; set; } + + public ChangellySettings ChangellySettings { get; set; } string _LightningDescriptionTemplate; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 80569bec4..ba0294710 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -38,6 +38,7 @@ using BTCPayServer.Logging; using BTCPayServer.HostedServices; using Meziantou.AspNetCore.BundleTagHelpers; using System.Security.Claims; +using BTCPayServer.Payments.Changelly; using BTCPayServer.Security; using Microsoft.AspNetCore.Mvc.ModelBinding; using NBXplorer.DerivationStrategy; @@ -125,6 +126,8 @@ namespace BTCPayServer.Hosting services.AddSingleton, Payments.Lightning.LightningLikePaymentHandler>(); services.AddSingleton(); + + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 78a13d50b..8f1dffeaa 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -55,7 +55,10 @@ namespace BTCPayServer.Models.InvoicingModels public string PaymentMethodName { get; set; } public string CryptoImage { get; set; } - public bool AllowCoinConversion { get; set; } + public bool ChangellyEnabled { get; set; } + public string StoreId { get; set; } public string PeerInfo { get; set; } + public string ChangellyMerchantId { get; set; } + public decimal? ChangellyAmountDue { get; set; } } } diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs index ae5176ffd..a77cccd0c 100644 --- a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs @@ -23,11 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels public string DefaultCryptoCurrency { get; set; } [Display(Name = "Default language on checkout")] public string DefaultLang { get; set; } - [Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")] - public bool AllowCoinConversion - { - get; set; - } [Display(Name = "Do not propose lightning payment if value of the invoice is above...")] [MaxLength(20)] public string LightningMaxValue { get; set; } diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index 9401bb957..6c899bf6d 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -21,7 +21,13 @@ namespace BTCPayServer.Models.StoreViewModels public WalletId WalletId { get; set; } public bool Enabled { get; set; } } - + + public class ThirdPartyPaymentMethod + { + public string Provider { get; set; } + public bool Enabled { get; set; } + public string Action { get; set; } + } public StoreViewModel() { @@ -52,6 +58,9 @@ namespace BTCPayServer.Models.StoreViewModels public List DerivationSchemes { get; set; } = new List(); + public List ThirdPartyPaymentMethods { get; set; } = + new List(); + [Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")] [Range(1, 60 * 24 * 24)] public int InvoiceExpiration diff --git a/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs b/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs new file mode 100644 index 000000000..62c468a45 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.InteropServices; +using BTCPayServer.Payments; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class UpdateChangellySettingsViewModel + { + [Required] + public string ApiKey { get; set; } + + [Required] + public string ApiSecret { get; set; } + + [Required] + public string ApiUrl { get; set; } = "https://api.changelly.com"; + + [Display(Name="Optional, Changelly Merchant Id")] + public string ChangellyMerchantId { get; set; } = "804298eb5753"; + + public bool Enabled { get; set; } = true; + + public string StatusMessage { get; set; } + + [Required] + [Range(0, 100)] + [Display(Name = "Percentage to multiply amount requested at Changelly to avoid underpaid situations due to Changelly not guaranteeing rates. ")] + public decimal AmountMarkupPercentage { get; set; } = new decimal(2); + + + + } +} diff --git a/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs b/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs new file mode 100644 index 000000000..0233e1940 --- /dev/null +++ b/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Services.Stores; +using Changelly.ResponseModel; + +namespace BTCPayServer.Payments.Changelly +{ + public class ChangellyClientProvider + { + private readonly StoreRepository _storeRepository; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + + + public ChangellyClientProvider(StoreRepository storeRepository, BTCPayNetworkProvider btcPayNetworkProvider) + { + _storeRepository = storeRepository; + _btcPayNetworkProvider = btcPayNetworkProvider; + } + + + public virtual bool TryGetChangellyClient(string storeId, out string error, + out global::Changelly.Changelly changelly) + { + changelly = null; + + + var store = _storeRepository.FindStore(storeId).Result; + if (store == null) + { + error = "Store not found"; + return false; + } + + var blob = store.GetStoreBlob(); + var changellySettings = blob.ChangellySettings; + + + if (changellySettings == null || !changellySettings.IsConfigured()) + { + error = "Changelly not configured for this store"; + return false; + } + + if (!changellySettings.Enabled) + { + error = "Changelly not enabled for this store"; + return false; + } + + changelly = new global::Changelly.Changelly(changellySettings.ApiKey, changellySettings.ApiSecret, + changellySettings.ApiUrl); + error = null; + return true; + } + + public virtual (IList currency, bool Success, string Error) GetCurrenciesFull(global::Changelly.Changelly client) + { + return client.GetCurrenciesFull(); + } + + public virtual (double amount, bool Success, string Error) GetExchangeAmount(global::Changelly.Changelly client, string fromCurrency, string toCurrency, + double amount) + { + + return client.GetExchangeAmount(fromCurrency, toCurrency, amount); + } + } +} diff --git a/BTCPayServer/Payments/Changelly/ChangellySettings.cs b/BTCPayServer/Payments/Changelly/ChangellySettings.cs new file mode 100644 index 000000000..784852a49 --- /dev/null +++ b/BTCPayServer/Payments/Changelly/ChangellySettings.cs @@ -0,0 +1,20 @@ +namespace BTCPayServer.Payments.Changelly +{ + public class ChangellySettings + { + public string ApiKey { get; set; } + public string ApiSecret { get; set; } + public string ApiUrl { get; set; } + public bool Enabled { get; set; } + public string ChangellyMerchantId { get; set; } + public decimal AmountMarkupPercentage { get; set; } + + public bool IsConfigured() + { + return + !string.IsNullOrEmpty(ApiKey) || + !string.IsNullOrEmpty(ApiSecret) || + !string.IsNullOrEmpty(ApiUrl); + } + } +} diff --git a/BTCPayServer/Payments/PaymentMethodExtensions.cs b/BTCPayServer/Payments/PaymentMethodExtensions.cs index af1f45e81..b63fe4364 100644 --- a/BTCPayServer/Payments/PaymentMethodExtensions.cs +++ b/BTCPayServer/Payments/PaymentMethodExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Payments.Changelly; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index 308b0e8fb..fb28f7a15 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -148,7 +148,7 @@
{{$t("Copy")}}
- @if (Model.AllowCoinConversion) + @if (Model.ChangellyEnabled) {
{{$t("Conversion")}} @@ -253,7 +253,7 @@
- @if (Model.AllowCoinConversion) + @if (Model.ChangellyEnabled) {
-
- - - - - - @*Changelly doesn't have TO_AMOUNT support so we can't include it - - - Changelly - *@ +
+ +
+
+ +
+ + Changelly + +
+ +
+
+
diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index 1a4bbd190..c5f86fdba 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -53,49 +53,53 @@
- -
-