mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Changelly Support (#267)
* Disable shapeshift and use changelly * UI to manage changelly payment method * wip on changelly api * Add in Vue component for changelly and remove target currency from payment method * add changelly merhcant id * Small fixes to get Conversion to load * wip fixing the component * fix merge conflict * fixes to UI * remove debug, fix fee calc and move changelly to own partials * Update ChangellyController.cs * move original vue setup back to checkout * Update core.js * Extracting Changelly component to js file * Proposal for loading spinner * remove zone * imrpove changelly ui * add in changelly config checks * try new method to calculate amount + remove to currency from list * abstract changelly lofgic to provider and reduce dependency on js component * Add UTs for Changelly * refactor changelly backend * fix failing UT * add shitcoin tax * pr changes * pr changes
This commit is contained in:
parent
e18d0b5d51
commit
a5fca7a1c4
23 changed files with 986 additions and 99 deletions
292
BTCPayServer.Tests/ChangellyTests.cs
Normal file
292
BTCPayServer.Tests/ChangellyTests.cs
Normal file
|
@ -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<StoresController>(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<RedirectToActionResult>(
|
||||
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<ChangellySettings>(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<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "url",
|
||||
ChangellyMerchantId = "aaa",
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
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<RedirectToActionResult>(
|
||||
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<ChangellyController>(user.UserId, user.StoreId);
|
||||
|
||||
//test non existing payment method
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(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<StoresController>(user.UserId, user.StoreId);
|
||||
//set payment method but disabled
|
||||
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
updateModel.Enabled = true;
|
||||
//test with enabled method
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
Assert.IsNotType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(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<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
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<CurrencyFull>()
|
||||
{
|
||||
new CurrencyFull()
|
||||
{
|
||||
Name = "a",
|
||||
Enable = true,
|
||||
PayInConfirmations = 10,
|
||||
FullName = "aa",
|
||||
ImageLink = ""
|
||||
}
|
||||
}, true, "");
|
||||
var result = ((IList<CurrencyFull> currency, bool Success, string Error))Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId)).Value;
|
||||
Assert.Equal(1, mock.GetCurrenciesFullCallCount);
|
||||
Assert.Equal(mock.GetCurrenciesFullResult.currency.Count, result.currency.Count);
|
||||
|
||||
mock.GetCurrenciesFullResult = (new List<CurrencyFull>()
|
||||
{
|
||||
new CurrencyFull()
|
||||
{
|
||||
Name = "a",
|
||||
Enable = true,
|
||||
PayInConfirmations = 10,
|
||||
FullName = "aa",
|
||||
ImageLink = ""
|
||||
}
|
||||
}, false, "");
|
||||
Assert
|
||||
.IsType<BadRequestObjectResult>(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<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
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<double>(Assert
|
||||
.IsType<OkObjectResult>(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<CurrencyFull> currency, bool Success, string Error) GetCurrenciesFullResult { get; set; }
|
||||
|
||||
public delegate TResult ParamsFunc<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
|
||||
|
||||
public ParamsFunc<string, string, double, (double amount, bool Success, string Error)> GetExchangeAmountResult
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int GetCurrenciesFullCallCount { get; set; } = 0;
|
||||
public int GetExchangeAmountCallCount { get; set; } = 0;
|
||||
|
||||
public override (IList<CurrencyFull> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="Changelly" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
|
|
103
BTCPayServer/Controllers/ChangellyController.cs
Normal file
103
BTCPayServer/Controllers/ChangellyController.cs
Normal file
|
@ -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<IActionResult> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
98
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
98
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
|
@ -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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
|
||||
|
||||
services.AddSingleton<ChangellyClientProvider>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public List<ThirdPartyPaymentMethod> ThirdPartyPaymentMethods { get; set; } =
|
||||
new List<ThirdPartyPaymentMethod>();
|
||||
|
||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||
[Range(1, 60 * 24 * 24)]
|
||||
public int InvoiceExpiration
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
68
BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs
Normal file
68
BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs
Normal file
|
@ -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<CurrencyFull> 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);
|
||||
}
|
||||
}
|
||||
}
|
20
BTCPayServer/Payments/Changelly/ChangellySettings.cs
Normal file
20
BTCPayServer/Payments/Changelly/ChangellySettings.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
<div class="payment-tabs__tab" id="copy-tab">
|
||||
<span>{{$t("Copy")}}</span>
|
||||
</div>
|
||||
@if (Model.AllowCoinConversion)
|
||||
@if (Model.ChangellyEnabled)
|
||||
{
|
||||
<div class="payment-tabs__tab" id="altcoins-tab">
|
||||
<span>{{$t("Conversion")}}</span>
|
||||
|
@ -253,7 +253,7 @@
|
|||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@if (Model.AllowCoinConversion)
|
||||
@if (Model.ChangellyEnabled)
|
||||
{
|
||||
<div id="altcoins" class="bp-view payment manual-flow">
|
||||
<nav v-if="srvModel.isLightning">
|
||||
|
@ -271,17 +271,32 @@
|
|||
{{$t("ConversionTab_BodyDesc", srvModel)}}
|
||||
</span>
|
||||
</div>
|
||||
<center>
|
||||
<script>function shapeshift_click(a, e) { e.preventDefault(); var link = a.href; var shapeshiftWindow = window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); shapeshiftWindow.focus(); return false; }</script>
|
||||
<a onclick="shapeshift_click(this, event);" v-bind:href="srvModel.shapeshiftUrl">
|
||||
<img src="https://shapeshift.io/images/shifty/xs_light_altcoins.png" class="ss-button">
|
||||
</a>
|
||||
|
||||
@*Changelly doesn't have TO_AMOUNT support so we can't include it
|
||||
<script type="text/javascript">function open_widget(a, e) { e.preventDefault(); var link = a.href; var changellyWindow = window.open(link, 'Changelly', 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); changellyWindow.focus(); return false; }</script>
|
||||
<a onclick="open_widget(this, event);" href="https://changelly.com/widget/v1?auth=email&from=DASH&to=BTC&address=&amount=1&merchant_id=&ref_id=">
|
||||
<img src="https://changelly.com/pay_button_pay_with.png" alt="Changelly" />
|
||||
</a>*@
|
||||
<center>
|
||||
<changelly inline-template
|
||||
:merchant-id="srvModel.changellyMerchantId"
|
||||
:store-id="srvModel.storeId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.changellyAmountDue"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div class="changelly-component">
|
||||
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
|
||||
<select
|
||||
v-model="selectedFromCurrency"
|
||||
:disabled="isLoading"
|
||||
v-on:change="onCurrencyChange($event)"
|
||||
ref="changellyCurrenciesDropdown">
|
||||
<option value="">Select a currency to convert from</option>
|
||||
<option v-for="currency of currencies" :value="currency.name">{{currency.fullName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<a v-on:click="openDialog($event)" :href="url" class="changelly-component-button">
|
||||
<img src="https://changelly.com/pay_button.png" alt="Changelly" v-show="url"/>
|
||||
</a>
|
||||
<div v-show="isLoading" class="general__spinner">
|
||||
<partial name="Checkout-Spinner"/>
|
||||
</div>
|
||||
</div>
|
||||
</changelly>
|
||||
</center>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -53,49 +53,53 @@
|
|||
</center>
|
||||
<![endif]-->
|
||||
|
||||
<invoice>
|
||||
<div class="no-bounce" id="checkoutCtrl" v-cloak>
|
||||
<div class="modal page">
|
||||
<div class="modal-dialog open opened enter-purchaser-email" role="document">
|
||||
<div class="modal-content long">
|
||||
<div class="content">
|
||||
<div class="invoice">
|
||||
<partial name="Checkout-Body" />
|
||||
</div>
|
||||
<invoice>
|
||||
<div class="no-bounce" id="checkoutCtrl" v-cloak>
|
||||
<div class="modal page">
|
||||
<div class="modal-dialog open opened enter-purchaser-email" role="document">
|
||||
<div class="modal-content long">
|
||||
<div class="content">
|
||||
<div class="invoice">
|
||||
<partial name="Checkout-Body" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; text-align: center;">
|
||||
@* Not working because of nsSeparator: false, keySeparator: false,
|
||||
<div style="margin-top: 10px; text-align: center;">
|
||||
@* Not working because of nsSeparator: false, keySeparator: false,
|
||||
{{$t("nested.lang")}} >>
|
||||
*@
|
||||
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
|
||||
@foreach (var lang in langService.GetLanguages())
|
||||
{
|
||||
<option value="@lang.Code">@lang.DisplayName</option>
|
||||
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
|
||||
@foreach (var lang in langService.GetLanguages())
|
||||
{
|
||||
<option value="@lang.Code">@lang.DisplayName</option>
|
||||
}
|
||||
</select>
|
||||
<script>
|
||||
$(function() {
|
||||
var storeDefaultLang = '@Model.DefaultLang';
|
||||
if (urlParams.lang) {
|
||||
$(".cmblang").val(urlParams.lang);
|
||||
} else if (storeDefaultLang) {
|
||||
$(".cmblang").val(storeDefaultLang);
|
||||
}
|
||||
</select>
|
||||
<script>
|
||||
$(function () {
|
||||
var storeDefaultLang = '@Model.DefaultLang';
|
||||
if (urlParams.lang) {
|
||||
$(".cmblang").val(urlParams.lang);
|
||||
} else if (storeDefaultLang) {
|
||||
$(".cmblang").val(storeDefaultLang);
|
||||
}
|
||||
|
||||
$('select').prettyDropdown({
|
||||
classic: false,
|
||||
height: 32,
|
||||
reverse: true,
|
||||
hoverIntent: 5000
|
||||
});
|
||||
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
|
||||
initDropdown(".cmblang");
|
||||
});
|
||||
|
||||
function initDropdown(selector) {
|
||||
return $(selector).prettyDropdown({
|
||||
classic: false,
|
||||
height: 32,
|
||||
reverse: true,
|
||||
hoverIntent: 5000
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div style="margin-top: 10px; text-align: center;" class="form-text small text-muted">
|
||||
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
|
||||
</div>
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<div style="margin-top: 10px; text-align: center;" class="form-text small text-muted">
|
||||
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -127,39 +131,40 @@
|
|||
},
|
||||
});
|
||||
|
||||
function changeLanguage(lang) {
|
||||
i18next.changeLanguage(lang);
|
||||
}
|
||||
function changeLanguage(lang) {
|
||||
i18next.changeLanguage(lang);
|
||||
}
|
||||
|
||||
if (urlParams.lang) {
|
||||
changeLanguage(urlParams.lang);
|
||||
}
|
||||
else if (storeDefaultLang) {
|
||||
changeLanguage(storeDefaultLang);
|
||||
}
|
||||
if (urlParams.lang) {
|
||||
changeLanguage(urlParams.lang);
|
||||
} else if (storeDefaultLang) {
|
||||
changeLanguage(storeDefaultLang);
|
||||
}
|
||||
|
||||
const i18n = new VueI18next(i18next);
|
||||
const i18n = new VueI18next(i18next);
|
||||
|
||||
// TODO: Move all logic from core.js to Vue controller
|
||||
Vue.config.ignoredElements = [
|
||||
'line-items',
|
||||
'low-fee-timeline',
|
||||
// Ignoring custom HTML5 elements, eg: bp-spinner
|
||||
/^bp-/
|
||||
];
|
||||
var checkoutCtrl = new Vue({
|
||||
i18n: i18n,
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
},
|
||||
data: {
|
||||
// TODO: Move all logic from core.js to Vue controller
|
||||
Vue.config.ignoredElements = [
|
||||
'line-items',
|
||||
'low-fee-timeline',
|
||||
// Ignoring custom HTML5 elements, eg: bp-spinner
|
||||
/^bp-/
|
||||
];
|
||||
|
||||
var checkoutCtrl = new Vue({
|
||||
i18n: i18n,
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr,
|
||||
changelly: ChangellyComponent
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel,
|
||||
lndModel: null,
|
||||
scanDisplayQr: "",
|
||||
expiringSoon: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -38,10 +38,6 @@
|
|||
<label asp-for="DefaultLang"></label>
|
||||
<select asp-for="DefaultLang" asp-items="Model.Languages" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AllowCoinConversion"></label>
|
||||
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresRefundEmail"></label>
|
||||
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check" />
|
||||
|
|
51
BTCPayServer/Views/Stores/UpdateChangellySettings.cshtml
Normal file
51
BTCPayServer/Views/Stores/UpdateChangellySettings.cshtml
Normal file
|
@ -0,0 +1,51 @@
|
|||
@using Microsoft.AspNetCore.Mvc.Rendering
|
||||
@model UpdateChangellySettingsViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store Changelly Settings");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiUrl"></label>
|
||||
<input asp-for="ApiUrl" class="form-control"/>
|
||||
<span asp-validation-for="ApiUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiKey"></label>
|
||||
<input asp-for="ApiKey" class="form-control"/>
|
||||
<span asp-validation-for="ApiKey" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiSecret"></label>
|
||||
<input asp-for="ApiSecret" class="form-control"/>
|
||||
<span asp-validation-for="ApiSecret" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ChangellyMerchantId"></label>
|
||||
<input asp-for="ChangellyMerchantId" class="form-control"/>
|
||||
<span asp-validation-for="ChangellyMerchantId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AmountMarkupPercentage"></label>
|
||||
<input asp-for="AmountMarkupPercentage" class="form-control"/>
|
||||
<span asp-validation-for="AmountMarkupPercentage" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
</div>
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
|
||||
<button name="command" type="submit" value="test" class="btn btn-primary">Test Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
|
@ -168,6 +168,43 @@
|
|||
Available placeholders are: {StoreName}, {ItemDescription} and {OrderId}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<h5>Third party Payment methods</h5>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th style="text-align:center;">Enabled</th>
|
||||
<th style="text-align:right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var scheme in Model.ThirdPartyPaymentMethods)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.Provider</td>
|
||||
<td style="text-align:center;">
|
||||
@if(scheme.Enabled)
|
||||
{
|
||||
<span class="fa fa-check"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="fa fa-times"></span>
|
||||
}
|
||||
</td>
|
||||
<td style="text-align:right"><a asp-action="@scheme.Action" >Modify</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(Model.CanDelete)
|
||||
{
|
||||
<div class="form-group">
|
||||
|
|
|
@ -9390,7 +9390,7 @@ strong {
|
|||
background-color: #fff;
|
||||
color: #000;
|
||||
font-size: 0px;
|
||||
width:100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btnGroupLnd button:first-child {
|
||||
|
@ -10995,6 +10995,15 @@ bp-spinner {
|
|||
opacity: .85;
|
||||
}
|
||||
|
||||
.general__spinner > bp-spinner > svg {
|
||||
margin: auto 0px 0px auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
fill: gray;
|
||||
animation: spin 0.55s linear infinite;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
bp-refund-address.ng-valid .bitcoin-logo {
|
||||
opacity: 1;
|
||||
margin-left: 0;
|
||||
|
@ -11459,3 +11468,16 @@ low-fee-timeline {
|
|||
opacity: 1;
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
|
||||
.changelly-component {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.changelly-component-dropdown-holder {
|
||||
height: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.changelly-component .general__spinner bp-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
|
115
BTCPayServer/wwwroot/checkout/js/changellyComponent.js
Normal file
115
BTCPayServer/wwwroot/checkout/js/changellyComponent.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
var ChangellyComponent =
|
||||
{
|
||||
props: ["storeId", "toCurrency", "toCurrencyDue", "toCurrencyAddress", "merchantId"],
|
||||
data: function () {
|
||||
return {
|
||||
currencies: [],
|
||||
isLoading: true,
|
||||
calculatedAmount: 0,
|
||||
selectedFromCurrency: "",
|
||||
prettyDropdownInstance: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
url: function () {
|
||||
if (this.calculatedAmount && this.selectedFromCurrency && !this.isLoading) {
|
||||
return "https://changelly.com/widget/v1?auth=email" +
|
||||
"&from=" +
|
||||
this.selectedFromCurrency +
|
||||
"&to=" +
|
||||
this.toCurrency +
|
||||
"&address=" +
|
||||
this.toCurrencyAddress +
|
||||
"&amount=" +
|
||||
this.calculatedAmount +
|
||||
(this.merchantId ? "&merchant_id=" + this.merchantId + "&ref_id=" + this.merchantId : "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedFromCurrency: function (val) {
|
||||
if (val) {
|
||||
this.calculateAmount();
|
||||
} else {
|
||||
this.calculateAmount = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.prettyDropdownInstance = initDropdown(this.$refs.changellyCurrenciesDropdown);
|
||||
this.loadCurrencies();
|
||||
},
|
||||
methods: {
|
||||
getUrl: function () {
|
||||
return window.location.origin + "/changelly/" + this.storeId;
|
||||
},
|
||||
loadCurrencies: function () {
|
||||
this.isLoading = true;
|
||||
$.ajax(
|
||||
{
|
||||
context: this,
|
||||
url: this.getUrl() + "/currencies",
|
||||
dataType: "json",
|
||||
success: function (result) {
|
||||
if (result.item2) {
|
||||
for (i = 0; i < result.item1.length; i++) {
|
||||
if (result.item1[i].enabled &&
|
||||
result.item1[i].name.toLowerCase() !== this.toCurrency.toLowerCase()) {
|
||||
this.currencies.push(result.item1[i]);
|
||||
}
|
||||
}
|
||||
var self = this;
|
||||
Vue.nextTick(function () {
|
||||
self.prettyDropdownInstance
|
||||
.refresh()
|
||||
.on("change",
|
||||
function (event) {
|
||||
self.onCurrencyChange(self.$refs.changellyCurrenciesDropdown.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
calculateAmount: function () {
|
||||
this.isLoading = true;
|
||||
$.ajax(
|
||||
{
|
||||
url: this.getUrl() + "/calculate",
|
||||
dataType: "json",
|
||||
data: {
|
||||
fromCurrency: this.selectedFromCurrency,
|
||||
toCurrency: this.toCurrency,
|
||||
toCurrencyAmount: this.toCurrencyDue
|
||||
},
|
||||
context: this,
|
||||
success: function (result) {
|
||||
this.calculatedAmount = result;
|
||||
},
|
||||
complete: function () {
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
onCurrencyChange: function (value) {
|
||||
this.selectedFromCurrency = value;
|
||||
},
|
||||
openDialog: function (e) {
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
var changellyWindow = window.open(
|
||||
this.url,
|
||||
'Changelly',
|
||||
'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0');
|
||||
changellyWindow.focus();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -32,9 +32,6 @@ function changeCurrency(currency) {
|
|||
}
|
||||
|
||||
function onDataCallback(jsonData) {
|
||||
// extender properties used
|
||||
jsonData.shapeshiftUrl = "https://shapeshift.io/shifty.html?destination=" + jsonData.btcAddress + "&output=" + jsonData.paymentMethodId + "&amount=" + jsonData.btcDue;
|
||||
//
|
||||
|
||||
var newStatus = jsonData.status;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue