mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
Make rate calculation scriptable
This commit is contained in:
parent
f460837f96
commit
6dc4bfaefe
@ -79,7 +79,7 @@ namespace BTCPayServer.Tests
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
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;
|
||||
await store.UpdateStore(vm);
|
||||
|
||||
|
@ -297,7 +297,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
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"));
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
@ -312,7 +312,7 @@ namespace BTCPayServer.Tests
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "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)));
|
||||
}
|
||||
}
|
||||
@ -678,9 +678,9 @@ namespace BTCPayServer.Tests
|
||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
||||
{
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
vm.PreferredExchange = exchange;
|
||||
storeController.UpdateStore(vm).Wait();
|
||||
storeController.Rates(vm).Wait();
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
@ -717,10 +717,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
|
||||
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);
|
||||
vm.RateMultiplier = 0.5;
|
||||
storeController.UpdateStore(vm).Wait();
|
||||
storeController.Rates(vm).Wait();
|
||||
|
||||
|
||||
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]
|
||||
public void CanPayWithTwoCurrencies()
|
||||
{
|
||||
@ -1255,7 +1315,7 @@ namespace BTCPayServer.Tests
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var coinAverage = new CoinAverageRateProvider(provider);
|
||||
var coinAverage = new CoinAverageRateProvider();
|
||||
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
|
||||
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();
|
||||
|
@ -460,9 +460,9 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -20,6 +21,7 @@ using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -33,6 +35,7 @@ namespace BTCPayServer.Controllers
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateFactory;
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
@ -46,12 +49,14 @@ namespace BTCPayServer.Controllers
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayRateProviderFactory rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
@ -191,6 +196,143 @@ namespace BTCPayServer.Controllers
|
||||
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]
|
||||
[Route("{storeId}/checkout")]
|
||||
public IActionResult CheckoutExperience()
|
||||
@ -268,7 +410,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}")]
|
||||
public IActionResult UpdateStore(string storeId)
|
||||
public IActionResult UpdateStore()
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
@ -276,7 +418,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
@ -285,7 +426,6 @@ namespace BTCPayServer.Controllers
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
return View(vm);
|
||||
}
|
||||
@ -328,13 +468,6 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}")]
|
||||
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);
|
||||
|
||||
bool needUpdate = false;
|
||||
@ -360,26 +493,11 @@ namespace BTCPayServer.Controllers
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
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)
|
||||
{
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
|
@ -200,13 +200,5 @@ namespace BTCPayServer
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
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;
|
||||
}
|
||||
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
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public class DerivationScheme
|
||||
{
|
||||
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 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")]
|
||||
[Range(1, 60 * 24 * 24)]
|
||||
public int InvoiceExpiration
|
||||
|
@ -30,6 +30,7 @@ namespace BTCPayServer.Rating
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
value = null;
|
||||
str = str.Trim();
|
||||
var splitted = str.Split('_');
|
||||
if (splitted.Length != 2)
|
||||
return false;
|
||||
|
@ -20,6 +20,7 @@ namespace BTCPayServer.Rating
|
||||
DivideByZero,
|
||||
PreprocessError,
|
||||
RateUnavailable,
|
||||
InvalidExchangeName,
|
||||
}
|
||||
public class RateRules
|
||||
{
|
||||
@ -28,13 +29,6 @@ namespace BTCPayServer.Rating
|
||||
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||
|
||||
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)
|
||||
{
|
||||
if (IsInvocation)
|
||||
@ -42,17 +36,22 @@ namespace BTCPayServer.Rating
|
||||
Errors.Add(RateRulesErrors.NestedInvocation);
|
||||
return base.VisitInvocationExpression(node);
|
||||
}
|
||||
IsInvocation = true;
|
||||
var result = base.VisitInvocationExpression(node);
|
||||
IsInvocation = false;
|
||||
return result;
|
||||
if (node.Expression is IdentifierNameSyntax id)
|
||||
{
|
||||
IsInvocation = true;
|
||||
var arglist = (ArgumentListSyntax)this.Visit(node.ArgumentList);
|
||||
IsInvocation = false;
|
||||
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)
|
||||
{
|
||||
if (IsInvocation)
|
||||
{
|
||||
return SyntaxFactory.IdentifierName(node.Identifier.ValueText.ToLowerInvariant());
|
||||
}
|
||||
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currencyPair))
|
||||
{
|
||||
return SyntaxFactory.IdentifierName(currencyPair.ToString())
|
||||
@ -70,7 +69,7 @@ namespace BTCPayServer.Rating
|
||||
public Dictionary<CurrencyPair, (ExpressionSyntax Expression, SyntaxNode Trivia)> ExpressionsByPair = new Dictionary<CurrencyPair, (ExpressionSyntax Expression, SyntaxNode Trivia)>();
|
||||
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
|
||||
{
|
||||
if (node.Kind() == SyntaxKind.SimpleAssignmentExpression
|
||||
if (node.Kind() == SyntaxKind.SimpleAssignmentExpression
|
||||
&& node.Left is IdentifierNameSyntax id
|
||||
&& node.Right is ExpressionSyntax expression)
|
||||
{
|
||||
@ -109,14 +108,22 @@ namespace BTCPayServer.Rating
|
||||
this.root = ruleList.GetSyntaxNode();
|
||||
}
|
||||
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;
|
||||
errors = null;
|
||||
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
|
||||
var rewriter = new NormalizeCurrencyPairsRewritter();
|
||||
// Rename BTC_usd to BTC_USD and verify structure
|
||||
var root = rewriter.Visit(expression.GetRoot());
|
||||
if (rewriter.Errors.Count > 0)
|
||||
{
|
||||
errors = rewriter.Errors;
|
||||
return false;
|
||||
}
|
||||
rules = new RateRules(root);
|
||||
return true;
|
||||
}
|
||||
@ -154,7 +161,7 @@ namespace BTCPayServer.Rating
|
||||
return CreateExpression($"ERR_NO_RULE_MATCH({p})");
|
||||
var best = candidates
|
||||
.OrderBy(c => c.Prioriy)
|
||||
.ThenBy(c => c.Expression.Span)
|
||||
.ThenBy(c => c.Expression.Span.Start)
|
||||
.First();
|
||||
|
||||
return best.Expression;
|
||||
|
@ -158,7 +158,7 @@ namespace BTCPayServer.Services.Rates
|
||||
providers.Add(directProvider);
|
||||
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
|
||||
{
|
||||
providers.Add(new CoinAverageRateProvider(btcpayNetworkProvider)
|
||||
providers.Add(new CoinAverageRateProvider()
|
||||
{
|
||||
Exchange = exchangeName,
|
||||
Authenticator = _CoinAverageSettings
|
||||
|
@ -55,13 +55,7 @@ namespace BTCPayServer.Services.Rates
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
public CoinAverageRateProvider()
|
||||
{
|
||||
|
||||
}
|
||||
public CoinAverageRateProvider(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
if (networkProvider == null)
|
||||
throw new ArgumentNullException(nameof(networkProvider));
|
||||
_NetworkProvider = networkProvider;
|
||||
_NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
|
||||
}
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
|
@ -30,6 +30,14 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
public string Name { 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>
|
||||
{
|
||||
|
@ -16,7 +16,7 @@
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<div id="help" class="collapse text-left">
|
||||
<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
|
||||
</p>
|
||||
<ul>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -125,7 +125,7 @@
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
<script type="text/javascript">
|
||||
@Model.ServerUrl.ToJSVariableModel("srvModel");
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model.ServerUrl));
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.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 Index => "Index";
|
||||
public static string Rates => "Rates";
|
||||
public static string Checkout => "Checkout experience";
|
||||
|
||||
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 IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
|
@ -34,19 +34,6 @@
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
</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">
|
||||
<label asp-for="InvoiceExpiration"></label>
|
||||
<input asp-for="InvoiceExpiration" class="form-control" />
|
||||
|
@ -65,7 +65,7 @@
|
||||
@section Scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.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">
|
||||
<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.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
|
||||
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
|
||||
|
Loading…
Reference in New Issue
Block a user