Compare commits

...

15 Commits

27 changed files with 452 additions and 147 deletions

View File

@ -138,6 +138,12 @@ namespace BTCPayServer.Tests
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),

View File

@ -46,8 +46,8 @@ namespace BTCPayServer.Tests
{
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
rules.GlobalMultiplier = 2.32m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
rules.Spread = 0.2m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
////////////////
// Check errors conditions
@ -113,22 +113,22 @@ namespace BTCPayServer.Tests
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
builder.AppendLine("DOGE_BTC = 2000");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rules.GlobalMultiplier = 1.1m;
rules.Spread = 0.1m;
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.BidAsk.Bid);
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.BidAsk.Bid);
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
////////
// Make sure kraken is not converted to CurrencyPair

View File

@ -745,20 +745,20 @@ namespace BTCPayServer.Tests
var rateController = acc.GetController<RateController>();
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId)
.GetAwaiter().GetResult()).ToJson()).ToObject<DataWrapper<Rate[]>>();
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetBaseCurrencyRatesResult);
Assert.NotNull(GetBaseCurrencyRatesResult.Data);
Assert.Single(GetBaseCurrencyRatesResult.Data);
Assert.Equal("LTC", GetBaseCurrencyRatesResult.Data.First().Code);
Assert.Equal(2, GetBaseCurrencyRatesResult.Data.Length);
Assert.Single(GetBaseCurrencyRatesResult.Data.Where(o => o.Code == "LTC"));
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId)
.GetAwaiter().GetResult()).ToJson()).ToObject<DataWrapper<Rate[]>>();
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetRatesResult);
Assert.NotNull(GetRatesResult.Data);
Assert.Equal(2, GetRatesResult.Data.Length);
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId)
.GetAwaiter().GetResult()).ToJson()).ToObject<DataWrapper<Rate>>();
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate>>();
Assert.NotNull(GetCurrencyPairRateResult);
Assert.NotNull(GetCurrencyPairRateResult.Data);
@ -982,8 +982,8 @@ namespace BTCPayServer.Tests
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@ -997,7 +997,11 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
// The rate was 5000 USD per BTC
// Now it should be 3000 USD per BTC
// So the expected price should be
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
}
}
@ -1079,7 +1083,7 @@ namespace BTCPayServer.Tests
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.Equal(0.0, rateVm.Spread);
Assert.Null(rateVm.TestRateRules);
rateVm.PreferredExchange = "bitflyer";
@ -1088,13 +1092,13 @@ namespace BTCPayServer.Tests
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
rateVm.RateMultiplier = 1.1;
rateVm.Spread = 10;
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.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.True(rateVm.TestRateRules[1].Error);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
@ -1107,19 +1111,19 @@ namespace BTCPayServer.Tests
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);
Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", 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.Spread = 50;
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.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
}
@ -1296,6 +1300,63 @@ namespace BTCPayServer.Tests
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
}
[Fact]
public void CanDisablePaymentMethods()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(3, invoice.CryptoInfo.Length);
var controller = user.GetController<StoresController>();
var lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.True(lightningVM.Enabled);
lightningVM.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult();
lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.False(lightningVM.Enabled);
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
// Confirmation
controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult();
Assert.False(derivationVM.Enabled);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.False(derivationVM.Enabled);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
}
}
[Fact]
public void CanSetPaymentMethodLimits()
{

View File

@ -0,0 +1,35 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDash()
{
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dash",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dash",
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",
"DASH_BTC = bittrex(DASH_BTC)"
},
CryptoImagePath = "imlegacy/dash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
: new KeyPath("1'"),
MinFee = Money.Satoshis(1m)
});
}
}
}

View File

@ -50,6 +50,7 @@ namespace BTCPayServer
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitDash();
InitPolis();
InitFeathercoin();
InitGroestlcoin();

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.81</Version>
<Version>1.0.2.83</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -123,9 +123,10 @@ namespace BTCPayServer.Controllers
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
@ -140,6 +141,7 @@ namespace BTCPayServer.Controllers
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,

View File

@ -46,21 +46,13 @@ namespace BTCPayServer.Controllers
err.StatusCode = 404;
return err;
}
var currencypairs = "";
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
.Select(method => method.PaymentId.CryptoCode).Distinct();
foreach (var currencyCode in currencyCodes)
{
if (!string.IsNullOrEmpty(currencypairs))
{
currencypairs += ",";
}
currencypairs += baseCurrency + "_ " + currencyCode;
}
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, store.Id);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -118,21 +110,11 @@ namespace BTCPayServer.Controllers
if (currencyPairs == null)
{
currencyPairs = "";
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
.Select(method => method.PaymentId.CryptoCode).Distinct();
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
foreach (var currencyCode in currencyCodes)
{
if (!string.IsNullOrEmpty(currencyPairs))
{
currencyPairs += ",";
}
currencyPairs += $"{defaultCrypto}_{currencyCode}";
}
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
if (string.IsNullOrEmpty(currencyPairs))
{
@ -160,7 +142,6 @@ namespace BTCPayServer.Controllers
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.AsParallel()
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
.Where(r => r.Value.HasValue)
.Select(r =>
@ -174,6 +155,20 @@ namespace BTCPayServer.Controllers
}).Where(n => n.Name != null).ToArray());
}
private static string BuildCurrencyPairs(IEnumerable<string> currencyCodes, string baseCrypto)
{
StringBuilder currencyPairsBuilder = new StringBuilder();
bool first = true;
foreach (var currencyCode in currencyCodes)
{
if(!first)
currencyPairsBuilder.Append(",");
first = false;
currencyPairsBuilder.Append($"{baseCrypto}_{currencyCode}");
}
return currencyPairsBuilder.ToString();
}
public class Rate
{

View File

@ -44,6 +44,7 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
}
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
@ -78,6 +79,11 @@ namespace BTCPayServer.Controllers
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationStrategy>()
.Select(c => c.DerivationStrategyBase.ToString())
.FirstOrDefault();
DerivationStrategy strategy = null;
try
{
@ -94,10 +100,32 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
if (!showAddress)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
store.SetStoreBlob(storeBlob);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
BitcoinAddress address = null;
try
@ -123,26 +151,8 @@ namespace BTCPayServer.Controllers
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
return ShowAddresses(vm, strategy);
}
else
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
return ShowAddresses(vm, strategy);
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)

View File

@ -24,9 +24,11 @@ namespace BTCPayServer.Controllers
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
LightningNodeViewModel vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
};
SetExistingValues(store, vm);
return View(vm);
}
@ -34,6 +36,7 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
@ -132,41 +135,43 @@ namespace BTCPayServer.Controllers
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethod.PaymentId , !vm.Enabled);
store.SetStoreBlob(storeBlob);
}
if (command == "save")
switch (command)
{
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else // if(command == "test")
{
if (paymentMethod == null)
{
case "save":
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
case "test" when paymentMethod == null:
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
return View(vm);
}
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
case "test":
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
{
await handler.TestConnection(info, cts.Token);
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
return View(vm);
default:
return View(vm);
}
}

View File

@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
model.AvailableExchanges = GetSupportedExchanges();
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.Spread = (decimal)model.Spread / 100.0m;
if (!model.ShowScripting)
{
@ -404,7 +404,7 @@ namespace BTCPayServer.Controllers
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, vm);
AddPaymentMethods(store, storeBlob, vm);
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
@ -413,8 +413,9 @@ namespace BTCPayServer.Controllers
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
@ -428,6 +429,7 @@ namespace BTCPayServer.Controllers
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
});
}
@ -439,10 +441,12 @@ namespace BTCPayServer.Controllers
foreach (var network in _NetworkProvider.GetAll())
{
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = network.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
Enabled = !excludeFilters.Match(paymentId)
});
}
}

View File

@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateRules.GlobalMultiplier = 1.0m;
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);

View File

@ -199,7 +199,7 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
{
return DefaultCrypto ?? (networkProvider == null? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{
@ -281,23 +281,9 @@ namespace BTCPayServer.Data
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule_Obsolete>();
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
public decimal Spread { get; set; } = 0.0m;
[Obsolete]
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
@ -336,15 +322,15 @@ namespace BTCPayServer.Data
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
{
if (!RateScripting ||
string.IsNullOrEmpty(RateScript) ||
if (!RateScripting ||
string.IsNullOrEmpty(RateScript) ||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
{
return GetDefaultRateRules(networkProvider);
}
else
{
rules.GlobalMultiplier = GetRateMultiplier();
rules.Spread = Spread;
return rules;
}
}
@ -370,8 +356,37 @@ namespace BTCPayServer.Data
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
rules.GlobalMultiplier = GetRateMultiplier();
rules.Spread = Spread;
return rules;
}
[Obsolete("Use GetExcludedPaymentMethods instead")]
public string[] ExcludedPaymentMethods { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (ExcludedPaymentMethods == null || ExcludedPaymentMethods.Length == 0)
return PaymentFilter.Never();
return PaymentFilter.Any(ExcludedPaymentMethods.Select(p => PaymentFilter.WhereIs(PaymentMethodId.Parse(p))).ToArray());
#pragma warning restore CS0618 // Type or member is obsolete
}
public bool IsExcluded(PaymentMethodId paymentMethodId)
{
return GetExcludedPaymentMethods().Match(paymentMethodId);
}
public void SetExcluded(PaymentMethodId paymentMethodId, bool value)
{
#pragma warning disable CS0618 // Type or member is obsolete
var methods = new HashSet<string>(ExcludedPaymentMethods ?? Array.Empty<string>());
if (value)
methods.Add(paymentMethodId.ToString());
else
methods.Remove(paymentMethodId.ToString());
ExcludedPaymentMethods = methods.ToArray();
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

View File

@ -53,6 +53,12 @@ namespace BTCPayServer.HostedServices
settings.UnreachableStoreCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertMultiplierToSpread)
{
await ConvertMultiplierToSpread();
settings.ConvertMultiplierToSpread = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -61,6 +67,30 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertMultiplierToSpread()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
var blob = store.GetStoreBlob();
#pragma warning disable CS0612 // Type or member is obsolete
decimal multiplier = 1.0m;
if (blob.RateRules != null && blob.RateRules.Count != 0)
{
foreach (var rule in blob.RateRules)
{
multiplier = rule.Apply(null, multiplier);
}
}
blob.RateRules = null;
blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m)));
#pragma warning restore CS0612 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
}
private Task UnreachableStoreCheck()
{
return _StoreRepository.CleanUnreachableStores();

View File

@ -27,6 +27,7 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Hint address")]
public string HintAddress { get; set; }
public bool Confirmation { get; set; }
public bool Enabled { get; set; } = true;
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }

View File

@ -24,5 +24,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public bool SkipPortTest { get; set; }
public bool Enabled { get; set; } = true;
}
}

View File

@ -44,9 +44,9 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
[Range(0.01, 10.0)]
public double RateMultiplier
[Display(Name = "Add a spread on exchange rate of ... %")]
[Range(0.0, 100.0)]
public double Spread
{
get;
set;

View File

@ -19,6 +19,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string Crypto { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public bool Enabled { get; set; }
}
public StoreViewModel()
@ -83,6 +84,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
public string CryptoCode { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
}
public List<LightningNode> LightningNodes
{

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments
{
public interface IPaymentFilter
{
bool Match(PaymentMethodId paymentMethodId);
}
public class PaymentFilter
{
class NeverPaymentFilter : IPaymentFilter
{
private static readonly NeverPaymentFilter _Instance = new NeverPaymentFilter();
public static NeverPaymentFilter Instance
{
get
{
return _Instance;
}
}
public bool Match(PaymentMethodId paymentMethodId)
{
return false;
}
}
class CompositePaymentFilter : IPaymentFilter
{
private readonly IPaymentFilter[] _filters;
public CompositePaymentFilter(IPaymentFilter[] filters)
{
_filters = filters;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return _filters.Any(f => f.Match(paymentMethodId));
}
}
class PaymentIdFilter : IPaymentFilter
{
private readonly PaymentMethodId _paymentMethodId;
public PaymentIdFilter(PaymentMethodId paymentMethodId)
{
_paymentMethodId = paymentMethodId;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return paymentMethodId == _paymentMethodId;
}
}
public static IPaymentFilter Never()
{
return NeverPaymentFilter.Instance;
}
public static IPaymentFilter Any(IPaymentFilter[] filters)
{
if (filters == null)
throw new ArgumentNullException(nameof(filters));
return new CompositePaymentFilter(filters);
}
public static IPaymentFilter WhereIs(PaymentMethodId paymentMethodId)
{
if (paymentMethodId == null)
throw new ArgumentNullException(nameof(paymentMethodId));
return new PaymentIdFilter(paymentMethodId);
}
}
}

View File

@ -99,7 +99,20 @@ namespace BTCPayServer.Rating
SyntaxNode root;
RuleList ruleList;
public decimal GlobalMultiplier { get; set; } = 1.0m;
decimal _Spread;
public decimal Spread
{
get
{
return _Spread;
}
set
{
if (value > 1.0m || value < 0.0m)
throw new ArgumentOutOfRangeException(paramName: nameof(value), message: "The spread should be between 0 and 1");
_Spread = value;
}
}
RateRules(SyntaxNode root)
{
@ -133,10 +146,12 @@ namespace BTCPayServer.Rating
{
if (currencyPair.Left == "X" || currencyPair.Right == "X")
throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency");
if (currencyPair.Left == currencyPair.Right)
return new RateRule(this, currencyPair, CreateExpression("1.0"));
var candidate = FindBestCandidate(currencyPair);
if (GlobalMultiplier != decimal.One)
if (Spread != decimal.Zero)
{
candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}");
candidate = CreateExpression($"({candidate}) * ({(1.0m - Spread).ToString(CultureInfo.InvariantCulture)}, {(1.0m + Spread).ToString(CultureInfo.InvariantCulture)})");
}
return new RateRule(this, currencyPair, candidate);
}
@ -255,7 +270,7 @@ namespace BTCPayServer.Rating
{
case SyntaxKind.UnaryMinusExpression:
var v = Values.Pop();
if(v.Bid == v.Ask)
if (v.Bid == v.Ask)
{
Values.Push(-v);
}
@ -336,7 +351,7 @@ namespace BTCPayServer.Rating
{
_TupleValues = new Stack<decimal>();
base.VisitTupleExpression(node);
if(_TupleValues.Count != 2)
if (_TupleValues.Count != 2)
{
Errors.Add(RateRulesErrors.MissingArgument);
}

View File

@ -9,5 +9,6 @@ namespace BTCPayServer.Services
{
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
}
}

View File

@ -1,7 +1,7 @@
@model DerivationSchemeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Add derivation scheme";
ViewData["Title"] = $"{Model.CryptoCode} Derivation scheme";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
@ -16,7 +16,7 @@
<div class="row">
<div class="col-md-8">
<form method="post">
@if (!Model.Confirmation)
@if(!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
@ -33,7 +33,7 @@
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<ul>
@for (int i = 0; i < 4; i++)
@for(int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
}
@ -77,7 +77,11 @@
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-primary">Continue</button>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check" />
</div>
<button name="command" type="submit" class="btn btn-primary" value="save">Continue</button>
}
else
{
@ -87,6 +91,7 @@
</div>
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="Enabled" />
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
@ -96,7 +101,7 @@
</tr>
</thead>
<tbody>
@foreach (var sample in Model.AddressSamples)
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
@ -116,7 +121,7 @@
<input asp-for="HintAddress" class="form-control" />
<span asp-validation-for="HintAddress" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
<button name="command" type="submit" class="btn btn-primary" value="save">Confirm</button>
}
</form>
</div>

View File

@ -62,6 +62,10 @@
</p>
}
</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-secondary">Test connection</button>
</form>

View File

@ -132,9 +132,9 @@
</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>
<label asp-for="Spread"></label>
<input asp-for="Spread" class="form-control" />
<span asp-validation-for="Spread" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Testing</h5>

View File

@ -5,6 +5,21 @@
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
<style type="text/css">
.smMaxWidth {
max-width: 150px;
}
@@media (min-width: 768px) {
.smMaxWidth {
max-width: 300px;
}
}
</style>
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
@ -70,7 +85,8 @@
<tr>
<th>Crypto</th>
<th>Derivation Scheme</th>
<th style="text-align:right">Actions</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:right;">Actions</th>
</tr>
</thead>
<tbody>
@ -78,7 +94,17 @@
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td class="smMaxWidth text-truncate">@scheme.Value</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">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
@ -106,6 +132,7 @@
<tr>
<th>Crypto</th>
<th>Address</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
@ -114,7 +141,17 @@
{
<tr>
<td>@scheme.CryptoCode</td>
<td>@scheme.Address</td>
<td class="smMaxWidth text-truncate">@scheme.Address</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="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
</tr>
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -29,6 +29,7 @@ In addition to Bitcoin, we support the following crypto currencies:
* Feathercoin
* Groestlcoin
* Litecoin
* Dash
* Monacoin
* Polis
* UFO
@ -72,4 +73,4 @@ On linux:
## Other dependencies
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Deployment.md).
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/#deployment).