Changelly v2 (#343)

* 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

* WIP: getting rid of changelly dependency

* client caching, compiling code, cleaner code

* Cleaner changelly

* fiat!

* updat i18n, css and error styler

* default keys

* pr changes part 1

* part2

* fix tests

* fix loader alignment and retry button responsiveness

* final pr change
This commit is contained in:
Andrew Camilleri 2018-10-24 07:52:19 +02:00 committed by Nicolas Dorier
parent f68d4efcdd
commit 4b976c13c1
29 changed files with 1274 additions and 102 deletions

View file

@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
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 = "http://gozo.com",
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 = "http://gozo.com",
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
{
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<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
}
}
[Fact]
public async void CanGetCurrencyListFromChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
//save changelly settings
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa"
};
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//confirm saved
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var mockChangelly = new MockChangelly(new MockHttpClientFactory(), updateModel.ApiKey, updateModel.ApiSecret, updateModel.ApiUrl);
var mock = new MockChangellyClientProvider(mockChangelly, tester.PayTester.StoreRepository);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var changellyController = new ChangellyController(mock, tester.NetworkProvider, fetcher);
mockChangelly.GetCurrenciesFullResult = new List<CurrencyFull>()
{
new CurrencyFull()
{
Name = "a",
Enable = true,
PayInConfirmations = 10,
FullName = "aa",
ImageLink = ""
}
};
var result = Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value as IEnumerable<CurrencyFull>;
Assert.Equal(1, mockChangelly.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 = "http://gozo.com",
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 mockChangelly = new MockChangelly(new MockHttpClientFactory(), updateModel.ApiKey, updateModel.ApiSecret, updateModel.ApiUrl);
var mock = new MockChangellyClientProvider(mockChangelly, tester.PayTester.StoreRepository);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var changellyController = new ChangellyController(mock,tester.NetworkProvider,fetcher);
mockChangelly.GetExchangeAmountResult = (from, to, amount) =>
{
Assert.Equal("A", from);
Assert.Equal("B", to);
switch (mockChangelly.GetExchangeAmountCallCount)
{
case 1:
return 0.5m;
break;
default:
return 1.01m;
break;
}
};
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "A", "B", 1.0m)).Value);
Assert.True(mockChangelly.GetExchangeAmountCallCount > 1);
}
}
}
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
public class MockChangelly : Changelly
{
public IEnumerable<CurrencyFull> GetCurrenciesFullResult { get; set; }
public delegate decimal ParamsFunc<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
public ParamsFunc<string, string, decimal, (decimal amount, bool Success, string Error)> GetExchangeAmountResult
{
get;
set;
}
public int GetCurrenciesFullCallCount { get; set; } = 0;
public int GetExchangeAmountCallCount { get; set; } = 0;
public MockChangelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl) : base(httpClientFactory, apiKey, apiSecret, apiUrl)
{
}
public override async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
{
GetCurrenciesFullCallCount++;
return GetCurrenciesFullResult;
}
public override async Task<decimal> GetExchangeAmount(string fromCurrency,
string toCurrency, decimal amount)
{
GetExchangeAmountCallCount++;
return GetExchangeAmountResult.Invoke(fromCurrency, toCurrency, amount);
}
}
public class MockChangellyClientProvider : ChangellyClientProvider
{
public MockChangelly MockChangelly;
public MockChangellyClientProvider(
MockChangelly mockChangelly,
StoreRepository storeRepository) : base(storeRepository, new MockHttpClientFactory())
{
MockChangelly = mockChangelly;
}
public override bool TryGetChangellyClient(string storeId, out string error, out Changelly changelly)
{
error = null;
changelly = MockChangelly;
return true;
}
}
}

View file

@ -1597,7 +1597,7 @@ namespace BTCPayServer.Tests
}
}
private static RateProviderFactory CreateBTCPayRateFactory()
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}

View file

@ -0,0 +1,125 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("[controller]/{storeId}")]
public class ChangellyController : Controller
{
private readonly ChangellyClientProvider _changellyClientProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly RateFetcher _RateProviderFactory;
public ChangellyController(ChangellyClientProvider changellyClientProvider,
BTCPayNetworkProvider btcPayNetworkProvider,
RateFetcher rateProviderFactory)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_changellyClientProvider = changellyClientProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[HttpGet]
[Route("currencies")]
public async Task<IActionResult> GetCurrencyList(string storeId)
{
if (!TryGetChangellyClient(storeId, out var actionResult, out var client))
{
return actionResult;
}
try
{
return Ok(await client.GetCurrenciesFull());
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
[HttpGet]
[Route("calculate")]
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
{
if (!TryGetChangellyClient(storeId, out var actionResult, out var client))
{
return actionResult;
}
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
{
var store = HttpContext.GetStoreData();
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
if (rate.BidAsk == null) return BadRequest();
var flatRate = rate.BidAsk.Center;
return Ok(flatRate * toCurrencyAmount);
}
try
{
var callCounter = 0;
var response1 = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
var currentAmount = response1;
while (true)
{
if (callCounter > 10)
{
BadRequest();
}
var response2 = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
callCounter++;
if (response2 < toCurrencyAmount)
{
var newCurrentAmount = ((toCurrencyAmount / response2) * 1m) * currentAmount;
currentAmount = newCurrentAmount;
}
else
{
return Ok(currentAmount);
}
}
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
private bool TryGetChangellyClient(string storeId, out IActionResult actionResult,
out Changelly changelly)
{
changelly = null;
actionResult = null;
storeId = storeId ?? HttpContext.GetStoreData()?.Id;
if (_changellyClientProvider.TryGetChangellyClient(storeId, out var error, out changelly))
return true;
actionResult = BadRequest(new BitpayErrorModel()
{
Error = error
});
return false;
}
}
}

View file

@ -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()

View file

@ -0,0 +1,98 @@
using System;
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;
vm.ShowFiat = existing.ShowFiat;
}
[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,
ShowFiat = vm.ShowFiat
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.ChangellySettings = changellySettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Changelly settings modified";
_changellyClientProvider.InvalidateClient(storeId);
return RedirectToAction(nameof(UpdateStore), new {
storeId});
case "test":
try
{
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl);
var result = await client.GetCurrenciesFull();
vm.StatusMessage = "Test Successful";
return View(vm);
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
break;
default:
return View(vm);
}
}
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
@ -9,6 +10,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;
@ -48,16 +50,19 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
ChangellyClientProvider changellyClientProvider,
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
{
_RateFactory = rateFactory;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_changellyClientProvider = changellyClientProvider;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
_httpClientFactory = httpClientFactory;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_FeeRateProvider = feeRateProvider;
@ -77,7 +82,9 @@ namespace BTCPayServer.Controllers
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
private readonly ChangellyClientProvider _changellyClientProvider;
IHostingEnvironment _Env;
private IHttpClientFactory _httpClientFactory;
[TempData]
public string StatusMessage
@ -318,7 +325,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 +368,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 +452,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]

View file

@ -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;

View file

@ -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>();

View file

@ -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; }
}
}

View file

@ -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; }

View file

@ -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

View file

@ -0,0 +1,33 @@
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; } = "6ed02cdf1b614d89a8c0ceb170eebb61";
[Required] public string ApiSecret { get; set; } = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483";
[Required] public string ApiUrl { get; set; } = "https://api.changelly.com";
[Display(Name = "Optional, Changelly Merchant Id")]
public string ChangellyMerchantId { get; set; } = "804298eb5753";
[Display(Name = "Show Fiat Currencies as option in conversion")]
public bool ShowFiat { get; set; } = true;
[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);
public bool Enabled { get; set; } = true;
public string StatusMessage { get; set; }
}
}

View file

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Changelly.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SshNet.Security.Cryptography;
namespace BTCPayServer.Payments.Changelly
{
public class Changelly
{
private readonly string _apisecret;
private readonly bool _showFiat;
private readonly HttpClient _httpClient;
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
{
_apisecret = apiSecret;
_showFiat = showFiat;
_httpClient = httpClientFactory.CreateClient();
_httpClient.BaseAddress = new Uri(apiUrl);
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
}
private static string ToHexString(byte[] array)
{
var hex = new StringBuilder(array.Length * 2);
foreach (var b in array)
{
hex.AppendFormat("{0:x2}", b);
}
return hex.ToString();
}
private async Task<ChangellyResponse<T>> PostToApi<T>(string message)
{
var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(_apisecret));
var hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
var sign = ToHexString(hashMessage);
var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Headers.Add("sign", sign);
request.Content = new StringContent(message, Encoding.UTF8, "application/json");
var result = await _httpClient.SendAsync(request);
if (!result.IsSuccessStatusCode)
throw new ChangellyException(result.ReasonPhrase);
var content =
await result.Content.ReadAsStringAsync();
return JObject.Parse(content).ToObject<ChangellyResponse<T>>();
}
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
{
const string message = @"{
""jsonrpc"": ""2.0"",
""id"": 1,
""method"": ""getCurrenciesFull"",
""params"": []
}";
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
var appendedResult = _showFiat
? result.Result.Concat(new[]
{
new CurrencyFull()
{
Enable = true,
Name = "EUR",
FullName = "Euro",
PayInConfirmations = 0,
ImageLink = "https://changelly.com/api/coins/eur.png"
},
new CurrencyFull()
{
Enable = true,
Name = "USD",
FullName = "US Dollar",
PayInConfirmations = 0,
ImageLink = "https://changelly.com/api/coins/usd.png"
}
})
: result.Result;
return appendedResult;
}
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
string toCurrency,
decimal amount)
{
var message =
$"{{\"id\": \"test\",\"jsonrpc\": \"2.0\",\"method\": \"getExchangeAmount\",\"params\":{{\"from\": \"{fromCurrency}\",\"to\": \"{toCurrency}\",\"amount\": \"{amount}\"}}}}";
var result = await PostToApi<string>(message);
return Convert.ToDecimal(result.Result);
}
}
}

View file

@ -0,0 +1,75 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using BTCPayServer.Services.Stores;
using NBitcoin;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyClientProvider
{
private readonly StoreRepository _storeRepository;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ConcurrentDictionary<string, Changelly> _clientCache =
new ConcurrentDictionary<string, Changelly>();
public ChangellyClientProvider(StoreRepository storeRepository, IHttpClientFactory httpClientFactory)
{
_storeRepository = storeRepository;
_httpClientFactory = httpClientFactory;
}
public void InvalidateClient(string storeId)
{
if (_clientCache.ContainsKey(storeId))
{
_clientCache.Remove(storeId, out var value);
}
}
public virtual bool TryGetChangellyClient(string storeId, out string error,
out Changelly changelly)
{
if (_clientCache.ContainsKey(storeId))
{
changelly = _clientCache[storeId];
error = null;
return true;
}
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 Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl, changellySettings.ShowFiat);
_clientCache.AddOrReplace(storeId, changelly);
error = null;
return true;
}
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyException : Exception
{
public ChangellyException(string message) : base(message)
{
}
}
}

View file

@ -0,0 +1,21 @@
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 ShowFiat { get; set; }
public bool IsConfigured()
{
return
!string.IsNullOrEmpty(ApiKey) ||
!string.IsNullOrEmpty(ApiSecret) ||
!string.IsNullOrEmpty(ApiUrl);
}
}
}

View file

@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class ChangellyResponse<T>
{
[JsonProperty("jsonrpc")]
public string JsonRPC { get; set; }
[JsonProperty("id")]
public object Id { get; set; }
[JsonProperty("result")]
public T Result { get; set; }
[JsonProperty("error")]
public Error Error { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class CurrencyFull
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("fullName")]
public string FullName { get; set; }
[JsonProperty("enabled")]
public bool Enable { get; set; }
[JsonProperty("payinConfirmations")]
public int PayInConfirmations { get; set; }
[JsonProperty("image")]
public string ImageLink { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class Error
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
}

View file

@ -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;

View file

@ -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,42 @@
{{$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"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
: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>
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
{{$t("ConversionTab_LoadCurrencies_Error")}}
</button>
<div v-show="isLoading" class="general__spinner">
<partial name="Checkout-Spinner"/>
</div>
</div>
</changelly>
</center>
</nav>
</div>

View file

@ -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>

View file

@ -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" />

View file

@ -0,0 +1,56 @@
@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="ShowFiat"></label>
<input asp-for="ShowFiat" class="form-check"/>
<span asp-validation-for="ShowFiat" 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")
}

View file

@ -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">

View file

@ -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,45 @@ 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;
}
.changelly-component .general__spinner bp-spinner svg {
margin: 0;
}
.changelly-component .retry-button {
border-radius: 5px;
border: 1px solid #a9a9a9;
background: #fff;
color: #000;
min-width:100px;
padding-left: 0.8rem;
padding-right: 0.8rem;
min-height: 30px;
word-break: break-all;
}
.changelly-component .retry-button :hover{
border-color: #7f7f7f;
}
.changelly-component .prettydropdown li img {
width:20px;
margin-right: 5px;
}

View file

@ -0,0 +1,131 @@
var ChangellyComponent =
{
props: ["storeId", "toCurrency", "toCurrencyDue", "toCurrencyAddress", "merchantId"],
data: function () {
return {
currencies: [],
isLoading: true,
calculatedAmount: 0,
selectedFromCurrency: "",
prettyDropdownInstance: null,
calculateError: false,
currenciesError: false
};
},
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;
this.currenciesError = false;
$.ajax(
{
context: this,
url: this.getUrl() + "/currencies",
dataType: "json",
success: function (result) {
for (i = 0; i < result.length; i++) {
if (result[i].enabled &&
result[i].name.toLowerCase() !== this.toCurrency.toLowerCase()) {
this.currencies.push(result[i]);
}
}
var self = this;
Vue.nextTick(function () {
self.prettyDropdownInstance
.refresh()
.on("change",
function (event) {
self.onCurrencyChange(self.$refs.changellyCurrenciesDropdown.value);
});
});
},
error: function(){
this.currenciesError = true;
},
complete: function () {
this.isLoading = false;
}
});
},
calculateAmount: function () {
this.isLoading = true;
this.calculateError = false;
$.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;
},
error: function(){
this.calculateError = true;
},
complete: function () {
this.isLoading = false;
}
});
},
retry: function(type){
if(type=="loadCurrencies"){
this.loadCurrencies();
}else if(type=="calculateAmount"){
this.calculateAmount();
}
},
onCurrencyChange: function (value) {
this.selectedFromCurrency = value;
this.calculatedAmount = 0;
},
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();
}
}
};

View file

@ -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;

View file

@ -27,7 +27,8 @@ const locales_en = {
// Conversion tab
"ConversionTab_BodyTop": "You can pay {{btcDue}} {{cryptoCode}} using altcoins other than the ones merchant directly supports.",
"ConversionTab_BodyDesc": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on {{cryptoCode}} Blockchain.",
"Shapeshift_Button_Text": "Pay with Altcoins",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_Lightning": "No conversion providers available for Lightning Network payments.",
// Invoice expired
"Invoice expiring soon...": "Invoice expiring soon...",