Fix rate handling

This commit is contained in:
nicolas.dorier 2018-04-15 21:18:51 +09:00
parent 7f01a12245
commit 0723eec508
13 changed files with 203 additions and 61 deletions

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Tests
} }
public Uri LTCNBXplorerUri { get; set; } public Uri LTCNBXplorerUri { get; set; }
public Uri ServerUri public Uri ServerUri
{ {
get; get;
@ -65,6 +65,9 @@ namespace BTCPayServer.Tests
get; set; get; set;
} }
public bool MockRates { get; set; } = true;
public void Start() public void Start()
{ {
if (!Directory.Exists(_Directory)) if (!Directory.Exists(_Directory))
@ -101,12 +104,15 @@ namespace BTCPayServer.Tests
.UseConfiguration(conf) .UseConfiguration(conf)
.ConfigureServices(s => .ConfigureServices(s =>
{ {
var mockRates = new MockRateProviderFactory(); if (MockRates)
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m)); {
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m)); var mockRates = new MockRateProviderFactory();
mockRates.AddMock(btc); var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
mockRates.AddMock(ltc); var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
s.AddSingleton<IRateProviderFactory>(mockRates); mockRates.AddMock(btc);
mockRates.AddMock(ltc);
s.AddSingleton<IRateProviderFactory>(mockRates);
}
s.AddLogging(l => s.AddLogging(l =>
{ {
l.SetMinimumLevel(LogLevel.Information) l.SetMinimumLevel(LogLevel.Information)
@ -121,7 +127,7 @@ namespace BTCPayServer.Tests
_Host.Start(); _Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
} }
public string HostName public string HostName
{ {
get; get;

View File

@ -34,21 +34,11 @@ namespace BTCPayServer.Tests
public ServerTester(string scope) public ServerTester(string scope)
{ {
_Directory = scope; _Directory = scope;
}
public bool Dockerized
{
get; set;
}
public void Start()
{
if (Directory.Exists(_Directory)) if (Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory); Utils.DeleteDirectory(_Directory);
if (!Directory.Exists(_Directory)) if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory); Directory.CreateDirectory(_Directory);
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest); NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork); ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork); LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
@ -72,6 +62,15 @@ namespace BTCPayServer.Tests
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture); PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false")); PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
}
public bool Dockerized
{
get; set;
}
public void Start()
{
PayTester.Start(); PayTester.Start();
} }

View File

@ -616,12 +616,51 @@ namespace BTCPayServer.Tests
} }
} }
[Fact]
public void CanUseExchangeSpecificRate()
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
rates.Add(CreateInvoice(tester, user, "bitflyer"));
foreach(var rate in rates)
{
Assert.Single(rates.Where(r => r == rate));
}
}
}
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
{
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
vm.PreferredExchange = exchange;
storeController.UpdateStore(user.StoreId, vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
return invoice2.CryptoInfo[0].Rate;
}
[Fact] [Fact]
public void CanTweakRate() public void CanTweakRate()
{ {
using (var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.PayTester.MockRates = false;
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.82</Version> <Version>1.0.1.83</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn> <NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers
{ {
var btc = _NetworkProvider.BTC; var btc = _NetworkProvider.BTC;
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc); var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc)); var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
if (feeProvider != null && rateProvider != null) if (feeProvider != null && rateProvider != null)
{ {
var gettingFee = feeProvider.GetFeeRateAsync(); var gettingFee = feeProvider.GetFeeRateAsync();
@ -186,7 +186,7 @@ namespace BTCPayServer.Controllers
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{ {
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network)).GetRateAsync(entity.ProductInformation.Currency); var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency);
PaymentMethod paymentMethod = new PaymentMethod(); PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity; paymentMethod.ParentEntity = entity;
paymentMethod.Network = network; paymentMethod.Network = network;
@ -221,7 +221,7 @@ namespace BTCPayServer.Controllers
if (limitValue.Currency == entity.ProductInformation.Currency) if (limitValue.Currency == entity.ProductInformation.Currency)
limitValueRate = paymentMethod.Rate; limitValueRate = paymentMethod.Rate;
else else
limitValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network)).GetRateAsync(limitValue.Currency); limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate); var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) if (compare(paymentMethod.Calculate().Due, limitValueCrypto))

View File

@ -49,18 +49,20 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork(cryptoCode); var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null) if (network == null)
return NotFound(); return NotFound();
var rateProvider = _RateProviderFactory.GetRateProvider(network);
if (rateProvider == null)
return NotFound();
RateRules rules = null;
if (storeId != null) if (storeId != null)
{ {
var store = await _StoreRepo.FindStore(storeId); var store = await _StoreRepo.FindStore(storeId);
if (store == null) if (store == null)
return NotFound(); return NotFound();
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider); rules = store.GetStoreBlob().GetRateRules();
} }
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
if (rateProvider == null)
return NotFound();
var allRates = (await rateProvider.GetRatesAsync()); var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r => return Json(allRates.Select(r =>
new NBitpayClient.Rate() new NBitpayClient.Rate()

View File

@ -284,30 +284,12 @@ namespace BTCPayServer.Data
} }
} }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider) public RateRules GetRateRules()
{ {
if (!PreferredExchange.IsCoinAverage()) return new RateRules(RateRules)
{ {
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all PreferredExchange = PreferredExchange
if (rateProvider is CachedRateProvider cachedRateProvider) };
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
cachedRateProvider.Inner
});
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
}
else
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
rateProvider
});
}
}
if (RateRules == null || RateRules.Count == 0)
return rateProvider;
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -6,24 +7,44 @@ using System.Threading.Tasks;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging;
namespace BTCPayServer.HostedServices namespace BTCPayServer.HostedServices
{ {
public class RatesHostedService : IHostedService public class RatesHostedService : IHostedService
{ {
private SettingsRepository _SettingsRepository; private SettingsRepository _SettingsRepository;
private BTCPayRateProviderFactory _RateProviderFactory; private IRateProviderFactory _RateProviderFactory;
public RatesHostedService(SettingsRepository repo, IRateProviderFactory rateProviderFactory) public RatesHostedService(SettingsRepository repo,
IRateProviderFactory rateProviderFactory)
{ {
this._SettingsRepository = repo; this._SettingsRepository = repo;
_RateProviderFactory = rateProviderFactory as BTCPayRateProviderFactory; _RateProviderFactory = rateProviderFactory;
} }
public async Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{
Init();
return Task.CompletedTask;
}
async void Init()
{ {
if (_RateProviderFactory == null)
return;
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting(); var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
//string[] availableExchanges = null;
//// So we don't run this in testing
//if(_RateProviderFactory is BTCPayRateProviderFactory)
//{
// try
// {
// await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync();
// }
// catch(Exception ex)
// {
// Logs.PayServer.LogWarning(ex, "Failed to get exchange tickers");
// }
//}
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)

View File

@ -51,9 +51,28 @@ namespace BTCPayServer.Services.Rates
_Cache = new MemoryCache(_CacheOptions); _Cache = new MemoryCache(_CacheOptions);
} }
public IRateProvider GetRateProvider(BTCPayNetwork network) public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
{ {
return new CachedRateProvider(network.CryptoCode, GetDefaultRateProvider(network), _Cache) { CacheSpan = CacheSpan }; rules = rules ?? new RateRules();
var rateProvider = GetDefaultRateProvider(network);
if (!rules.PreferredExchange.IsCoinAverage())
{
rateProvider = CreateExchangeRateProvider(network, rules.PreferredExchange);
}
rateProvider = CreateCachedRateProvider(network, rateProvider, rules.PreferredExchange);
return new TweakRateProvider(network, rateProvider, rules);
}
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
{
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
coinAverage.Exchange = exchange;
return coinAverage;
}
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
{
return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope };
} }
private IRateProvider GetDefaultRateProvider(BTCPayNetwork network) private IRateProvider GetDefaultRateProvider(BTCPayNetwork network)

View File

@ -30,13 +30,31 @@ namespace BTCPayServer.Services.Rates
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{ {
return new CoinAverageRateProvider(CryptoCode) return new CoinAverageRateProvider(CryptoCode)
{ {
Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>() Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>()
}; };
} }
IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider)
{
return CreateRateProvider(serviceProvider);
}
}
public class GetExchangeTickersResponse
{
public class Exchange
{
public string Name { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
public string[] Symbols { get; set; }
}
public bool Success { get; set; }
public Exchange[] Exchanges { get; set; }
} }
public class RatesSetting public class RatesSetting
@ -181,5 +199,26 @@ namespace BTCPayServer.Services.Rates
var resp = await _Client.SendAsync(request); var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode(); resp.EnsureSuccessStatusCode();
} }
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();
response.Success = jobj["success"].Value<bool>();
var exchanges = (JObject)jobj["exchanges"];
response.Exchanges = exchanges
.Properties()
.Select(p =>
{
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
exchange.Name = p.Name;
return exchange;
})
.ToArray();
return response;
}
} }
} }

View File

@ -1,12 +1,40 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class RateRules : IEnumerable<RateRule>
{
private List<RateRule> rateRules;
public RateRules()
{
rateRules = new List<RateRule>();
}
public RateRules(List<RateRule> rateRules)
{
this.rateRules = rateRules?.ToList() ?? new List<RateRule>();
}
public string PreferredExchange { get; set; }
public IEnumerator<RateRule> GetEnumerator()
{
return rateRules.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public interface IRateProviderFactory public interface IRateProviderFactory
{ {
IRateProvider GetRateProvider(BTCPayNetwork network); IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules);
TimeSpan CacheSpan { get; set; }
void InvalidateCache();
} }
} }

View File

@ -14,14 +14,21 @@ namespace BTCPayServer.Services.Rates
} }
public TimeSpan CacheSpan { get; set; }
public void AddMock(MockRateProvider mock) public void AddMock(MockRateProvider mock)
{ {
_Mocks.Add(mock); _Mocks.Add(mock);
} }
public IRateProvider GetRateProvider(BTCPayNetwork network) public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
{ {
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode); return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
} }
public void InvalidateCache()
{
}
} }
public class MockRateProvider : IRateProvider public class MockRateProvider : IRateProvider
{ {

View File

@ -10,9 +10,9 @@ namespace BTCPayServer.Services.Rates
{ {
private BTCPayNetwork network; private BTCPayNetwork network;
private IRateProvider rateProvider; private IRateProvider rateProvider;
private List<RateRule> rateRules; private RateRules rateRules;
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List<RateRule> rateRules) public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules)
{ {
if (network == null) if (network == null)
throw new ArgumentNullException(nameof(network)); throw new ArgumentNullException(nameof(network));