Compare commits

...

10 Commits

12 changed files with 148 additions and 45 deletions

View File

@ -35,9 +35,9 @@ namespace BTCPayServer.Tests
rules.ToString());
var tests = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
};
@ -81,9 +81,9 @@ namespace BTCPayServer.Tests
var tests2 = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
};
@ -129,6 +129,14 @@ namespace BTCPayServer.Tests
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.Value.Value);
////////
// Make sure kraken is not converted to CurrencyPair
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), 1000m);
Assert.True(rule2.Reevaluate());
}
}
}

View File

@ -36,6 +36,7 @@ using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
namespace BTCPayServer.Tests
@ -48,6 +49,27 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanHandleUriValidation()
{
var attribute = new UriAttribute();
Assert.True(attribute.IsValid("http://localhost"));
Assert.True(attribute.IsValid("http://localhost:1234"));
Assert.True(attribute.IsValid("https://localhost"));
Assert.True(attribute.IsValid("https://127.0.0.1"));
Assert.True(attribute.IsValid("http://127.0.0.1"));
Assert.True(attribute.IsValid("http://127.0.0.1:1234"));
Assert.True(attribute.IsValid("http://gozo.com"));
Assert.True(attribute.IsValid("https://gozo.com"));
Assert.True(attribute.IsValid("https://gozo.com:1234"));
Assert.True(attribute.IsValid("https://gozo.com:1234/test.css"));
Assert.True(attribute.IsValid("https://gozo.com:1234/test.png"));
Assert.False(attribute.IsValid("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud e"));
Assert.False(attribute.IsValid(2));
Assert.False(attribute.IsValid("http://"));
Assert.False(attribute.IsValid("httpdsadsa.com"));
}
[Fact]
public void CanCalculateCryptoDue2()
{
@ -265,7 +287,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// Set tolerance to 50%
var stores = user.GetController<StoresController>();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(stores.UpdateStore()).Model);
@ -296,6 +318,22 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void RoundupCurrenciesCorrectly()
{
foreach(var test in new[]
{
(0.0005m, "$0.0005 (USD)"),
(0.001m, "$0.001 (USD)"),
(0.01m, "$0.01 (USD)"),
(0.1m, "$0.10 (USD)"),
})
{
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
Assert.Equal(test.Item2, actual);
}
}
[Fact]
public void CanPayUsingBIP70()
{
@ -617,7 +655,7 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
@ -1411,7 +1449,7 @@ namespace BTCPayServer.Tests
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
foreach (var result in factory
.DirectProviders
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
@ -1423,8 +1461,8 @@ namespace BTCPayServer.Tests
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => ( e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
&& e.Value > 1.0m // 1BTC will always be more than 1USD
@ -1454,7 +1492,7 @@ namespace BTCPayServer.Tests
private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider)
{
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, null, new CoinAverageSettings());
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
}
[Fact]
@ -1470,7 +1508,6 @@ namespace BTCPayServer.Tests
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(provider);
factory.DirectProviders.Clear();
factory.CacheSpan = TimeSpan.FromSeconds(10);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();

View File

@ -89,7 +89,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.14-dev
image: nicolasdorier/clightning:0.0.0.16-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |

View File

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

View File

@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
"low",
@ -61,7 +61,7 @@ namespace BTCPayServer.Controllers
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
@ -291,11 +291,29 @@ namespace BTCPayServer.Controllers
private string FormatCurrency(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public string FormatCurrency(decimal price, string currency)
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
var provider = ((CultureInfo)currencies.GetCurrencyProvider(currency)).NumberFormat;
var currencyData = currencies.GetCurrencyData(currency);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if(divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
return price.ToString("C", provider) + $" ({currency})";
}
[HttpGet]
@ -430,7 +448,7 @@ namespace BTCPayServer.Controllers
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if(store == null)
if (store == null)
{
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
}

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.InvoicingModels
{
@ -52,8 +53,7 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
[Url]
[Uri]
public string NotificationUrl
{
get; set;

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
@ -42,10 +43,10 @@ namespace BTCPayServer.Models.StoreViewModels
public string OnChainMinValue { get; set; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Url]
[Uri]
public string CustomCSS { get; set; }
[Display(Name = "Link to a custom logo")]
[Url]
[Uri]
public string CustomLogo { get; set; }
[Display(Name = "Custom HTML title to display on Checkout page")]

View File

@ -1,6 +1,7 @@
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validation;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
@ -34,7 +35,7 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Url]
[Uri]
[Display(Name = "Store Website")]
[MaxLength(500)]
public string StoreWebsite

View File

@ -347,17 +347,23 @@ namespace BTCPayServer.Rating
class FlattenExpressionRewriter : CSharpSyntaxRewriter
{
RateRules parent;
CurrencyPair pair;
int nested = 0;
public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair)
{
Context.Push(pair);
this.pair = pair;
this.parent = parent;
}
public ExchangeRates ExchangeRates = new ExchangeRates();
public Stack<CurrencyPair> Context { get; set; } = new Stack<CurrencyPair>();
bool IsInvocation;
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
{
if (IsInvocation)
{
Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier);
return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})");
}
IsInvocation = true;
_ExchangeName = node.Expression.ToString();
var result = base.VisitInvocationExpression(node);
@ -365,18 +371,27 @@ namespace BTCPayServer.Rating
return result;
}
bool IsArgumentList;
public override SyntaxNode VisitArgumentList(ArgumentListSyntax node)
{
IsArgumentList = true;
var result = base.VisitArgumentList(node);
IsArgumentList = false;
return result;
}
string _ExchangeName = null;
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
const int MaxNestedCount = 8;
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
{
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
if (
(!IsInvocation || IsArgumentList) &&
CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
{
var ctx = Context.Peek();
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? ctx.Left : currentPair.Left,
right: currentPair.Right == "X" ? ctx.Right : currentPair.Right);
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? pair.Left : currentPair.Left,
right: currentPair.Right == "X" ? pair.Right : currentPair.Right);
if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD)
{
ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName });
@ -385,13 +400,13 @@ namespace BTCPayServer.Rating
else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD
{
var bestCandidate = parent.FindBestCandidate(replacedPair);
if (Context.Count > MaxNestedCount)
if (nested > MaxNestedCount)
{
Errors.Add(RateRulesErrors.TooMuchNestedCalls);
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
}
Context.Push(replacedPair);
var replaced = Visit(bestCandidate);
var innerFlatten = CreateNewContext(replacedPair);
var replaced = innerFlatten.Visit(bestCandidate);
if (replaced is ExpressionSyntax expression)
{
var hasBinaryOps = new HasBinaryOperations();
@ -401,7 +416,6 @@ namespace BTCPayServer.Rating
replaced = SyntaxFactory.ParenthesizedExpression(expression);
}
}
Context.Pop();
if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls))
{
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
@ -411,6 +425,16 @@ namespace BTCPayServer.Rating
}
return base.VisitIdentifierName(node);
}
private FlattenExpressionRewriter CreateNewContext(CurrencyPair pair)
{
return new FlattenExpressionRewriter(parent, pair)
{
Errors = Errors,
nested = nested + 1,
ExchangeRates = ExchangeRates,
};
}
}
private SyntaxNode expression;
FlattenExpressionRewriter flatten;

View File

@ -36,7 +36,6 @@ namespace BTCPayServer.Services.Rates
}
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
CurrencyNameTable _CurrencyTable;
public IMemoryCache Cache
{
get
@ -47,12 +46,10 @@ namespace BTCPayServer.Services.Rates
CoinAverageSettings _CoinAverageSettings;
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
BTCPayNetworkProvider btcpayNetworkProvider,
CurrencyNameTable currencyTable,
CoinAverageSettings coinAverageSettings)
{
if (cacheOptions == null)
throw new ArgumentNullException(nameof(cacheOptions));
_CurrencyTable = currencyTable;
_CoinAverageSettings = coinAverageSettings;
_Cache = new MemoryCache(cacheOptions);
_CacheOptions = cacheOptions;
@ -71,6 +68,7 @@ namespace BTCPayServer.Services.Rates
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
@ -89,13 +87,14 @@ namespace BTCPayServer.Services.Rates
public CoinAverageExchanges GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach(var exchange in _CoinAverageSettings.AvailableExchanges)
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
{
exchanges.Add(exchange.Value);
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
@ -178,13 +177,6 @@ namespace BTCPayServer.Services.Rates
}
rateRule.Reevaluate();
result.Value = rateRule.Value;
var currencyData = _CurrencyTable?.GetCurrencyData(rateRule.CurrencyPair.Right);
if(currencyData != null && result.Value.HasValue)
{
result.Value = decimal.Round(result.Value.Value, currencyData.Divisibility, MidpointRounding.AwayFromZero);
}
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);

View File

@ -0,0 +1,22 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace BTCPayServer.Validation
{
//from https://stackoverflow.com/a/47196738/275504
public class UriAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Uri uri;
bool valid = Uri.TryCreate(Convert.ToString(value, CultureInfo.InvariantCulture), UriKind.Absolute, out uri);
if (!valid)
{
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
}

2
run.sh
View File

@ -1,3 +1,3 @@
#!/bin/bash
dotnet run --no-launch-profile --no-build -c Release -p "BTCPayServer/BTCPayServer.csproj" -- "$@"
dotnet run --no-launch-profile --no-build -c Release -p "BTCPayServer/BTCPayServer.csproj" -- $@