mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Make rate calculation scriptable
This commit is contained in:
@@ -79,7 +79,7 @@ namespace BTCPayServer.Tests
|
|||||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||||
var vm = (StoreViewModel)((ViewResult)store.UpdateStore(StoreId)).Model;
|
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
|
||||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||||
await store.UpdateStore(vm);
|
await store.UpdateStore(vm);
|
||||||
|
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ namespace BTCPayServer.Tests
|
|||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
user.GrantAccess();
|
||||||
var storeController = user.GetController<StoresController>();
|
var storeController = user.GetController<StoresController>();
|
||||||
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId));
|
Assert.IsType<ViewResult>(storeController.UpdateStore());
|
||||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
|
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
|
||||||
|
|
||||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||||
@@ -312,7 +312,7 @@ namespace BTCPayServer.Tests
|
|||||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||||
}, "save", "BTC").GetAwaiter().GetResult());
|
}, "save", "BTC").GetAwaiter().GetResult());
|
||||||
|
|
||||||
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId)).Model);
|
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore()).Model);
|
||||||
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
|
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,9 +678,9 @@ namespace BTCPayServer.Tests
|
|||||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
||||||
{
|
{
|
||||||
var storeController = user.GetController<StoresController>();
|
var storeController = user.GetController<StoresController>();
|
||||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
|
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||||
vm.PreferredExchange = exchange;
|
vm.PreferredExchange = exchange;
|
||||||
storeController.UpdateStore(vm).Wait();
|
storeController.Rates(vm).Wait();
|
||||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0,
|
||||||
@@ -717,10 +717,10 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
|
|
||||||
var storeController = user.GetController<StoresController>();
|
var storeController = user.GetController<StoresController>();
|
||||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
|
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||||
Assert.Equal(1.0, vm.RateMultiplier);
|
Assert.Equal(1.0, vm.RateMultiplier);
|
||||||
vm.RateMultiplier = 0.5;
|
vm.RateMultiplier = 0.5;
|
||||||
storeController.UpdateStore(vm).Wait();
|
storeController.Rates(vm).Wait();
|
||||||
|
|
||||||
|
|
||||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||||
@@ -796,6 +796,66 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanModifyRates()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
var store = user.GetController<StoresController>();
|
||||||
|
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.False(rateVm.ShowScripting);
|
||||||
|
Assert.Equal("coinaverage", rateVm.PreferredExchange);
|
||||||
|
Assert.Equal(1.0, rateVm.RateMultiplier);
|
||||||
|
Assert.Null(rateVm.TestRateRules);
|
||||||
|
|
||||||
|
rateVm.PreferredExchange = "bitflyer";
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.Equal("bitflyer", rateVm.PreferredExchange);
|
||||||
|
|
||||||
|
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
|
||||||
|
rateVm.RateMultiplier = 1.1;
|
||||||
|
store = user.GetController<StoresController>();
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||||
|
Assert.NotNull(rateVm.TestRateRules);
|
||||||
|
Assert.Equal(2, rateVm.TestRateRules.Count);
|
||||||
|
Assert.False(rateVm.TestRateRules[0].Error);
|
||||||
|
Assert.StartsWith("(bitflyer(BTC_JPY)) * 1.10 =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.True(rateVm.TestRateRules[1].Error);
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
store = user.GetController<StoresController>();
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
|
||||||
|
Assert.True(rateVm.ShowScripting);
|
||||||
|
rateVm.ScriptTest = "BTC_JPY";
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||||
|
Assert.True(rateVm.ShowScripting);
|
||||||
|
Assert.Contains("(bitflyer(BTC_JPY)) * 1.10 = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
|
||||||
|
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
|
||||||
|
"X_CAD = quadrigacx(X_CAD);\n" +
|
||||||
|
"X_X = gdax(X_X);";
|
||||||
|
rateVm.RateMultiplier = 0.5;
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||||
|
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
store = user.GetController<StoresController>();
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.Equal(0.5, rateVm.RateMultiplier);
|
||||||
|
Assert.True(rateVm.ShowScripting);
|
||||||
|
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanPayWithTwoCurrencies()
|
public void CanPayWithTwoCurrencies()
|
||||||
{
|
{
|
||||||
@@ -1255,7 +1315,7 @@ namespace BTCPayServer.Tests
|
|||||||
public void CheckRatesProvider()
|
public void CheckRatesProvider()
|
||||||
{
|
{
|
||||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||||
var coinAverage = new CoinAverageRateProvider(provider);
|
var coinAverage = new CoinAverageRateProvider();
|
||||||
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
|
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
|
||||||
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
|
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
|
||||||
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
|
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
|
||||||
|
|||||||
@@ -460,9 +460,9 @@ namespace BTCPayServer.Controllers
|
|||||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||||
return RedirectToAction(nameof(ListInvoices));
|
return RedirectToAction(nameof(ListInvoices));
|
||||||
}
|
}
|
||||||
catch (BitpayHttpException)
|
catch (BitpayHttpException ex)
|
||||||
{
|
{
|
||||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using BTCPayServer.Data;
|
|||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
@@ -20,6 +21,7 @@ using NBitcoin.DataEncoders;
|
|||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -33,6 +35,7 @@ namespace BTCPayServer.Controllers
|
|||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public partial class StoresController : Controller
|
public partial class StoresController : Controller
|
||||||
{
|
{
|
||||||
|
BTCPayRateProviderFactory _RateFactory;
|
||||||
public string CreatedStoreId { get; set; }
|
public string CreatedStoreId { get; set; }
|
||||||
public StoresController(
|
public StoresController(
|
||||||
NBXplorerDashboard dashboard,
|
NBXplorerDashboard dashboard,
|
||||||
@@ -46,12 +49,14 @@ namespace BTCPayServer.Controllers
|
|||||||
AccessTokenController tokenController,
|
AccessTokenController tokenController,
|
||||||
BTCPayWalletProvider walletProvider,
|
BTCPayWalletProvider walletProvider,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
|
BTCPayRateProviderFactory rateFactory,
|
||||||
ExplorerClientProvider explorerProvider,
|
ExplorerClientProvider explorerProvider,
|
||||||
IFeeProviderFactory feeRateProvider,
|
IFeeProviderFactory feeRateProvider,
|
||||||
LanguageService langService,
|
LanguageService langService,
|
||||||
IHostingEnvironment env,
|
IHostingEnvironment env,
|
||||||
CoinAverageSettings coinAverage)
|
CoinAverageSettings coinAverage)
|
||||||
{
|
{
|
||||||
|
_RateFactory = rateFactory;
|
||||||
_Dashboard = dashboard;
|
_Dashboard = dashboard;
|
||||||
_Repo = repo;
|
_Repo = repo;
|
||||||
_TokenRepository = tokenRepo;
|
_TokenRepository = tokenRepo;
|
||||||
@@ -191,6 +196,143 @@ namespace BTCPayServer.Controllers
|
|||||||
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
|
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/rates")]
|
||||||
|
public IActionResult Rates()
|
||||||
|
{
|
||||||
|
var storeBlob = StoreData.GetStoreBlob();
|
||||||
|
var vm = new RatesViewModel();
|
||||||
|
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||||
|
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||||
|
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||||
|
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||||
|
vm.AvailableExchanges = GetSupportedExchanges();
|
||||||
|
vm.ShowScripting = storeBlob.RateScripting;
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/rates")]
|
||||||
|
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||||
|
{
|
||||||
|
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
if (model.PreferredExchange != null)
|
||||||
|
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
var blob = StoreData.GetStoreBlob();
|
||||||
|
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||||
|
model.AvailableExchanges = GetSupportedExchanges();
|
||||||
|
|
||||||
|
blob.PreferredExchange = model.PreferredExchange;
|
||||||
|
blob.SetRateMultiplier(model.RateMultiplier);
|
||||||
|
|
||||||
|
if (!model.ShowScripting)
|
||||||
|
{
|
||||||
|
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RateRules rules = null;
|
||||||
|
if (model.ShowScripting)
|
||||||
|
{
|
||||||
|
if (!RateRules.TryParse(model.Script, out rules, out var errors))
|
||||||
|
{
|
||||||
|
errors = errors ?? new List<RateRulesErrors>();
|
||||||
|
var errorString = String.Join(", ", errors.ToArray());
|
||||||
|
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
blob.RateScript = rules.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rules = blob.GetRateRules(_NetworkProvider);
|
||||||
|
|
||||||
|
if (command == "Test")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(model.ScriptTest))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
var pairs = new List<CurrencyPair>();
|
||||||
|
foreach (var pair in splitted)
|
||||||
|
{
|
||||||
|
if (!CurrencyPair.TryParse(pair, out var currencyPair))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
pairs.Add(currencyPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||||
|
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||||
|
foreach (var fetch in fetchs)
|
||||||
|
{
|
||||||
|
var testResult = await (fetch.Value);
|
||||||
|
testResults.Add(new RatesViewModel.TestResultViewModel()
|
||||||
|
{
|
||||||
|
CurrencyPair = fetch.Key.ToString(),
|
||||||
|
Error = testResult.Errors.Count != 0,
|
||||||
|
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: testResult.EvaluatedRule
|
||||||
|
});
|
||||||
|
}
|
||||||
|
model.TestRateRules = testResults;
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
else // command == Save
|
||||||
|
{
|
||||||
|
if (StoreData.SetStoreBlob(blob))
|
||||||
|
{
|
||||||
|
await _Repo.UpdateStore(StoreData);
|
||||||
|
StatusMessage = "Rate settings updated";
|
||||||
|
}
|
||||||
|
return RedirectToAction(nameof(Rates), new
|
||||||
|
{
|
||||||
|
storeId = StoreData.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/rates/confirm")]
|
||||||
|
public IActionResult ShowRateRules(bool scripting)
|
||||||
|
{
|
||||||
|
return View("Confirm", new ConfirmModel()
|
||||||
|
{
|
||||||
|
Action = nameof(ShowRateRulesPost),
|
||||||
|
Title = "Rate rule scripting",
|
||||||
|
Description = scripting ?
|
||||||
|
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||||
|
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||||
|
ButtonClass = "btn-primary"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/rates/confirm")]
|
||||||
|
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||||
|
{
|
||||||
|
var blob = StoreData.GetStoreBlob();
|
||||||
|
blob.RateScripting = scripting;
|
||||||
|
blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||||
|
StoreData.SetStoreBlob(blob);
|
||||||
|
await _Repo.UpdateStore(StoreData);
|
||||||
|
StatusMessage = "Rate rules scripting activated";
|
||||||
|
return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/checkout")]
|
[Route("{storeId}/checkout")]
|
||||||
public IActionResult CheckoutExperience()
|
public IActionResult CheckoutExperience()
|
||||||
@@ -268,7 +410,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}")]
|
[Route("{storeId}")]
|
||||||
public IActionResult UpdateStore(string storeId)
|
public IActionResult UpdateStore()
|
||||||
{
|
{
|
||||||
var store = HttpContext.GetStoreData();
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
@@ -276,7 +418,6 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
var vm = new StoreViewModel();
|
var vm = new StoreViewModel();
|
||||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
|
||||||
vm.Id = store.Id;
|
vm.Id = store.Id;
|
||||||
vm.StoreName = store.StoreName;
|
vm.StoreName = store.StoreName;
|
||||||
vm.StoreWebsite = store.StoreWebsite;
|
vm.StoreWebsite = store.StoreWebsite;
|
||||||
@@ -285,7 +426,6 @@ namespace BTCPayServer.Controllers
|
|||||||
AddPaymentMethods(store, vm);
|
AddPaymentMethods(store, vm);
|
||||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
|
||||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
@@ -328,13 +468,6 @@ namespace BTCPayServer.Controllers
|
|||||||
[Route("{storeId}")]
|
[Route("{storeId}")]
|
||||||
public async Task<IActionResult> UpdateStore(StoreViewModel model)
|
public async Task<IActionResult> UpdateStore(StoreViewModel model)
|
||||||
{
|
{
|
||||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
if (model.PreferredExchange != null)
|
|
||||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
|
||||||
AddPaymentMethods(StoreData, model);
|
AddPaymentMethods(StoreData, model);
|
||||||
|
|
||||||
bool needUpdate = false;
|
bool needUpdate = false;
|
||||||
@@ -360,26 +493,11 @@ namespace BTCPayServer.Controllers
|
|||||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||||
|
|
||||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
|
||||||
blob.PreferredExchange = model.PreferredExchange;
|
|
||||||
|
|
||||||
blob.SetRateMultiplier(model.RateMultiplier);
|
|
||||||
|
|
||||||
if (StoreData.SetStoreBlob(blob))
|
if (StoreData.SetStoreBlob(blob))
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newExchange)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needUpdate)
|
if (needUpdate)
|
||||||
{
|
{
|
||||||
await _Repo.UpdateStore(StoreData);
|
await _Repo.UpdateStore(StoreData);
|
||||||
|
|||||||
@@ -200,13 +200,5 @@ namespace BTCPayServer
|
|||||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HtmlString ToJSVariableModel(this object o, string variableName)
|
|
||||||
{
|
|
||||||
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
|
|
||||||
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ namespace BTCPayServer.Models
|
|||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
public string ButtonClass { get; set; } = "btn-danger";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
{
|
||||||
|
public class RatesViewModel
|
||||||
|
{
|
||||||
|
public class TestResultViewModel
|
||||||
|
{
|
||||||
|
public string CurrencyPair { get; set; }
|
||||||
|
public string Rule { get; set; }
|
||||||
|
public bool Error { get; set; }
|
||||||
|
}
|
||||||
|
class Format
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||||
|
{
|
||||||
|
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||||
|
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||||
|
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||||
|
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||||
|
PreferredExchange = chosen.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||||
|
|
||||||
|
public SelectList Exchanges { get; set; }
|
||||||
|
|
||||||
|
public bool ShowScripting { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Rate rules")]
|
||||||
|
[MaxLength(2000)]
|
||||||
|
public string Script { get; set; }
|
||||||
|
public string DefaultScript { get; set; }
|
||||||
|
public string ScriptTest { get; set; }
|
||||||
|
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Multiply the original rate by ...")]
|
||||||
|
[Range(0.01, 10.0)]
|
||||||
|
public double RateMultiplier
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||||
|
public string PreferredExchange { get; set; }
|
||||||
|
|
||||||
|
public string RateSource
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,11 +13,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
public class StoreViewModel
|
public class StoreViewModel
|
||||||
{
|
{
|
||||||
class Format
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Value { get; set; }
|
|
||||||
}
|
|
||||||
public class DerivationScheme
|
public class DerivationScheme
|
||||||
{
|
{
|
||||||
public string Crypto { get; set; }
|
public string Crypto { get; set; }
|
||||||
@@ -50,36 +45,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
|
|
||||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||||
|
|
||||||
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
|
||||||
{
|
|
||||||
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
|
||||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
|
||||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
|
||||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
|
||||||
PreferredExchange = chosen.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SelectList Exchanges { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
|
||||||
public string PreferredExchange { get; set; }
|
|
||||||
|
|
||||||
public string RateSource
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Display(Name = "Multiply the original rate by ...")]
|
|
||||||
[Range(0.01, 10.0)]
|
|
||||||
public double RateMultiplier
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||||
[Range(1, 60 * 24 * 24)]
|
[Range(1, 60 * 24 * 24)]
|
||||||
public int InvoiceExpiration
|
public int InvoiceExpiration
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ namespace BTCPayServer.Rating
|
|||||||
if (str == null)
|
if (str == null)
|
||||||
throw new ArgumentNullException(nameof(str));
|
throw new ArgumentNullException(nameof(str));
|
||||||
value = null;
|
value = null;
|
||||||
|
str = str.Trim();
|
||||||
var splitted = str.Split('_');
|
var splitted = str.Split('_');
|
||||||
if (splitted.Length != 2)
|
if (splitted.Length != 2)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace BTCPayServer.Rating
|
|||||||
DivideByZero,
|
DivideByZero,
|
||||||
PreprocessError,
|
PreprocessError,
|
||||||
RateUnavailable,
|
RateUnavailable,
|
||||||
|
InvalidExchangeName,
|
||||||
}
|
}
|
||||||
public class RateRules
|
public class RateRules
|
||||||
{
|
{
|
||||||
@@ -28,13 +29,6 @@ namespace BTCPayServer.Rating
|
|||||||
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||||
|
|
||||||
bool IsInvocation;
|
bool IsInvocation;
|
||||||
public override SyntaxNode VisitArgumentList(ArgumentListSyntax node)
|
|
||||||
{
|
|
||||||
IsInvocation = false;
|
|
||||||
var result = base.VisitArgumentList(node);
|
|
||||||
IsInvocation = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||||
{
|
{
|
||||||
if (IsInvocation)
|
if (IsInvocation)
|
||||||
@@ -42,17 +36,22 @@ namespace BTCPayServer.Rating
|
|||||||
Errors.Add(RateRulesErrors.NestedInvocation);
|
Errors.Add(RateRulesErrors.NestedInvocation);
|
||||||
return base.VisitInvocationExpression(node);
|
return base.VisitInvocationExpression(node);
|
||||||
}
|
}
|
||||||
|
if (node.Expression is IdentifierNameSyntax id)
|
||||||
|
{
|
||||||
IsInvocation = true;
|
IsInvocation = true;
|
||||||
var result = base.VisitInvocationExpression(node);
|
var arglist = (ArgumentListSyntax)this.Visit(node.ArgumentList);
|
||||||
IsInvocation = false;
|
IsInvocation = false;
|
||||||
return result;
|
return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(id.Identifier.ValueText.ToLowerInvariant()), arglist)
|
||||||
|
.WithTriviaFrom(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.InvalidExchangeName);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||||
{
|
{
|
||||||
if (IsInvocation)
|
|
||||||
{
|
|
||||||
return SyntaxFactory.IdentifierName(node.Identifier.ValueText.ToLowerInvariant());
|
|
||||||
}
|
|
||||||
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currencyPair))
|
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currencyPair))
|
||||||
{
|
{
|
||||||
return SyntaxFactory.IdentifierName(currencyPair.ToString())
|
return SyntaxFactory.IdentifierName(currencyPair.ToString())
|
||||||
@@ -109,14 +108,22 @@ namespace BTCPayServer.Rating
|
|||||||
this.root = ruleList.GetSyntaxNode();
|
this.root = ruleList.GetSyntaxNode();
|
||||||
}
|
}
|
||||||
public static bool TryParse(string str, out RateRules rules)
|
public static bool TryParse(string str, out RateRules rules)
|
||||||
|
{
|
||||||
|
return TryParse(str, out rules, out var unused);
|
||||||
|
}
|
||||||
|
public static bool TryParse(string str, out RateRules rules, out List<RateRulesErrors> errors)
|
||||||
{
|
{
|
||||||
rules = null;
|
rules = null;
|
||||||
|
errors = null;
|
||||||
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
|
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
|
||||||
var rewriter = new NormalizeCurrencyPairsRewritter();
|
var rewriter = new NormalizeCurrencyPairsRewritter();
|
||||||
// Rename BTC_usd to BTC_USD and verify structure
|
// Rename BTC_usd to BTC_USD and verify structure
|
||||||
var root = rewriter.Visit(expression.GetRoot());
|
var root = rewriter.Visit(expression.GetRoot());
|
||||||
if (rewriter.Errors.Count > 0)
|
if (rewriter.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
errors = rewriter.Errors;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
rules = new RateRules(root);
|
rules = new RateRules(root);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -154,7 +161,7 @@ namespace BTCPayServer.Rating
|
|||||||
return CreateExpression($"ERR_NO_RULE_MATCH({p})");
|
return CreateExpression($"ERR_NO_RULE_MATCH({p})");
|
||||||
var best = candidates
|
var best = candidates
|
||||||
.OrderBy(c => c.Prioriy)
|
.OrderBy(c => c.Prioriy)
|
||||||
.ThenBy(c => c.Expression.Span)
|
.ThenBy(c => c.Expression.Span.Start)
|
||||||
.First();
|
.First();
|
||||||
|
|
||||||
return best.Expression;
|
return best.Expression;
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ namespace BTCPayServer.Services.Rates
|
|||||||
providers.Add(directProvider);
|
providers.Add(directProvider);
|
||||||
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
|
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
|
||||||
{
|
{
|
||||||
providers.Add(new CoinAverageRateProvider(btcpayNetworkProvider)
|
providers.Add(new CoinAverageRateProvider()
|
||||||
{
|
{
|
||||||
Exchange = exchangeName,
|
Exchange = exchangeName,
|
||||||
Authenticator = _CoinAverageSettings
|
Authenticator = _CoinAverageSettings
|
||||||
|
|||||||
@@ -55,13 +55,7 @@ namespace BTCPayServer.Services.Rates
|
|||||||
BTCPayNetworkProvider _NetworkProvider;
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
public CoinAverageRateProvider()
|
public CoinAverageRateProvider()
|
||||||
{
|
{
|
||||||
|
_NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
|
||||||
}
|
|
||||||
public CoinAverageRateProvider(BTCPayNetworkProvider networkProvider)
|
|
||||||
{
|
|
||||||
if (networkProvider == null)
|
|
||||||
throw new ArgumentNullException(nameof(networkProvider));
|
|
||||||
_NetworkProvider = networkProvider;
|
|
||||||
}
|
}
|
||||||
static HttpClient _Client = new HttpClient();
|
static HttpClient _Client = new HttpClient();
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ namespace BTCPayServer.Services.Rates
|
|||||||
}
|
}
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Display { get; set; }
|
public string Display { get; set; }
|
||||||
|
public string Url
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
|
||||||
|
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@Model.ToJSVariableModel("srvModel")
|
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
|
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
|
||||||
<div id="help" class="collapse text-left">
|
<div id="help" class="collapse text-left">
|
||||||
<p>
|
<p>
|
||||||
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
|
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.<br />
|
||||||
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters
|
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 text-center">
|
<div class="col-lg-12 text-center">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<button type="submit" class="btn btn-secondary btn-danger" title="Continue">@Model.Action</button>
|
<button type="submit" class="btn btn-secondary @Model.ButtonClass" title="Continue">@Model.Action</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
@section Scripts {
|
@section Scripts {
|
||||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@Model.ServerUrl.ToJSVariableModel("srvModel");
|
var srvModel = @Html.Raw(Json.Serialize(Model.ServerUrl));
|
||||||
</script>
|
</script>
|
||||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||||
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
|
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
|
||||||
|
|||||||
149
BTCPayServer/Views/Stores/Rates.cshtml
Normal file
149
BTCPayServer/Views/Stores/Rates.cshtml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
@model RatesViewModel
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewData["Title"] = "Rates";
|
||||||
|
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Rates);
|
||||||
|
}
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<form method="post">
|
||||||
|
@if(Model.ShowScripting)
|
||||||
|
{
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>Scripting</h5>
|
||||||
|
<span>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</span>
|
||||||
|
<p class="text-muted">
|
||||||
|
<b>Supported exchanges are</b>:
|
||||||
|
@for(int i = 0; i < Model.AvailableExchanges.Length; i++)
|
||||||
|
{
|
||||||
|
<a href="@Model.AvailableExchanges[i].Url">@Model.AvailableExchanges[i].Name</a><span>@(i == Model.AvailableExchanges.Length - 1 ? "" : ",")</span>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<p><a href="#help" data-toggle="collapse"><b>Click here for more information</b></a></p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(Model.TestRateRules != null)
|
||||||
|
{
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>Test results:</h5>
|
||||||
|
<table class="table table-sm table-responsive-md">
|
||||||
|
<tbody>
|
||||||
|
@foreach(var result in Model.TestRateRules)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
@if(result.Error)
|
||||||
|
{
|
||||||
|
<th class="small"><span class="fa fa-times" style="color:red;"></span> @result.CurrencyPair</th>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<th class="small"><span class="fa fa-check" style="color:green;"></span> @result.CurrencyPair</th>
|
||||||
|
}
|
||||||
|
<td>@result.Rule</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(Model.ShowScripting)
|
||||||
|
{
|
||||||
|
<div id="help" class="collapse text-left">
|
||||||
|
<p>
|
||||||
|
The script language is composed of several rules composed of a currency pair and a mathematic expression.
|
||||||
|
The example below will use <code>gdax</code> for both <code>LTC_USD</code> and <code>BTC_USD</code> pairs.
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
LTC_USD = gdax(LTC_USD);
|
||||||
|
BTC_USD = gdax(BTC_USD);
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<p>However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule <code>X_X</code> which will match any currency pair. The following example will use <code>gdax</code> for getting the rate of any currency pair.</p>
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
X_X = gdax(X_X);
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<p>However, <code>gdax</code> does not support the <code>BTC_CAD</code> pair. For this reason you can add a rule mapping all <code>X_CAD</code> to <code>quadrigacx</code>, a Canadian exchange.</p>
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
X_CAD = quadrigacx(X_CAD);
|
||||||
|
X_X = gdax(X_X);
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<p>A given currency pair match the most specific rule. If two rules are matching and are as specific, the first rule will be chosen.</p>
|
||||||
|
<p>
|
||||||
|
But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most exchange do not have any pair for it. But <code>bittrex</code> has a <code>DOGE_BTC</code> pair. <br />
|
||||||
|
Luckily, the rule engine allow you to reference rules:
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
DOGE_X = bittrex(DOGE_BTC) * BTC_X
|
||||||
|
X_CAD = quadrigacx(X_CAD);
|
||||||
|
X_X = gdax(X_X);
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<p>With <code>DOGE_USD</code> will be expanded to <code>bittrex(DOGE_BTC) * gdax(BTC_USD)</code>. And <code>DOGE_CAD</code> will be expanded to <code>bittrex(DOGE_BTC) * quadrigacx(BTC_CAD)</code></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Script"></label>
|
||||||
|
<textarea asp-for="Script" rows="20" cols="80" class="form-control"></textarea>
|
||||||
|
<span asp-validation-for="Script" class="text-danger"></span>
|
||||||
|
<a href="#" onclick="$('#Script').val(defaultScript); return false;">Set to default settings</a>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<a asp-action="ShowRateRules" asp-route-scripting="false">Turn off advanced rate rule scripting</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="PreferredExchange"></label>
|
||||||
|
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||||
|
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||||
|
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||||
|
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<a asp-action="ShowRateRules" asp-route-scripting="true">Turn on advanced rate rule scripting</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="RateMultiplier"></label>
|
||||||
|
<input asp-for="RateMultiplier" class="form-control" />
|
||||||
|
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>Testing</h5>
|
||||||
|
<span>Enter currency pairs which you want to test against your rule (eg. <code>DOGE_USD,DOGE_CAD,BTC_CAD,BTC_USD</code>)</span>
|
||||||
|
<div class="input-group">
|
||||||
|
<input placeholder="BTC_USD, BTC_CAD" asp-for="ScriptTest" class="form-control" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button name="command" value="Test" type="submit" class="btn btn-primary" title="Test">
|
||||||
|
<span class="fa fa-vial"></span> Test
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="ScriptTest" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||||
|
<input type="hidden" asp-for="ShowScripting" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script type="text/javascript">var defaultScript = @Html.Raw(Json.Serialize(Model.DefaultScript));</script>
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Stores
|
|||||||
{
|
{
|
||||||
public static string ActivePageKey => "ActivePage";
|
public static string ActivePageKey => "ActivePage";
|
||||||
public static string Index => "Index";
|
public static string Index => "Index";
|
||||||
|
public static string Rates => "Rates";
|
||||||
public static string Checkout => "Checkout experience";
|
public static string Checkout => "Checkout experience";
|
||||||
|
|
||||||
public static string Tokens => "Tokens";
|
public static string Tokens => "Tokens";
|
||||||
@@ -20,6 +21,7 @@ namespace BTCPayServer.Views.Stores
|
|||||||
|
|
||||||
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
|
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
|
||||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||||
|
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
|
||||||
|
|
||||||
public static string PageNavClass(ViewContext viewContext, string page)
|
public static string PageNavClass(ViewContext viewContext, string page)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,19 +34,6 @@
|
|||||||
<label asp-for="NetworkFee"></label>
|
<label asp-for="NetworkFee"></label>
|
||||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="PreferredExchange"></label>
|
|
||||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
|
||||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
|
||||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
|
||||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="RateMultiplier"></label>
|
|
||||||
<input asp-for="RateMultiplier" class="form-control" />
|
|
||||||
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="InvoiceExpiration"></label>
|
<label asp-for="InvoiceExpiration"></label>
|
||||||
<input asp-for="InvoiceExpiration" class="form-control" />
|
<input asp-for="InvoiceExpiration" class="form-control" />
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
@section Scripts
|
@section Scripts
|
||||||
{
|
{
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@Model.ToJSVariableModel("srvModel")
|
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||||
</script>
|
</script>
|
||||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||||
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<div class="nav flex-column nav-pills">
|
<div class="nav flex-column nav-pills">
|
||||||
<a class="nav-link @StoreNavPages.IndexNavClass(ViewContext)" asp-action="UpdateStore">General settings</a>
|
<a class="nav-link @StoreNavPages.IndexNavClass(ViewContext)" asp-action="UpdateStore">General settings</a>
|
||||||
|
<a class="nav-link @StoreNavPages.RatesNavClass(ViewContext)" asp-action="Rates">Rates</a>
|
||||||
<a class="nav-link @StoreNavPages.CheckoutNavClass(ViewContext)" asp-action="CheckoutExperience">Checkout experience</a>
|
<a class="nav-link @StoreNavPages.CheckoutNavClass(ViewContext)" asp-action="CheckoutExperience">Checkout experience</a>
|
||||||
<a class="nav-link @StoreNavPages.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
|
<a class="nav-link @StoreNavPages.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
|
||||||
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
|
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user