Refactor the RateProvider

This commit is contained in:
nicolas.dorier 2018-05-03 03:32:42 +09:00
parent 43be1e191f
commit e57a488371
34 changed files with 583 additions and 616 deletions

View File

@ -2,6 +2,7 @@
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
@ -106,15 +107,6 @@ namespace BTCPayServer.Tests
.UseConfiguration(conf)
.ConfigureServices(s =>
{
if (MockRates)
{
var mockRates = new MockRateProviderFactory();
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
mockRates.AddMock(btc);
mockRates.AddMock(ltc);
s.AddSingleton<IRateProviderFactory>(mockRates);
}
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
@ -128,6 +120,30 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
Value = 5000m
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
Value = 4500m
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
Value = 500m
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
}
public string HostName

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Tests.Mocks
{
public class MockRateProvider : IRateProvider
{
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public Task<ExchangeRates> GetRatesAsync()
{
return Task.FromResult(ExchangeRates);
}
}
}

View File

@ -41,7 +41,8 @@ namespace BTCPayServer.Tests
{
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"), 2.32m).ToString());
rules.GlobalMultiplier = 2.32m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
////////////////
// Check errors conditions
@ -107,7 +108,8 @@ 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));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"), 1.1m);
rules.GlobalMultiplier = 1.1m;
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
Assert.True(rule2.Reevaluate());

View File

@ -35,6 +35,7 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
using BTCPayServer.Rating;
namespace BTCPayServer.Tests
{
@ -108,22 +109,11 @@ namespace BTCPayServer.Tests
{
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
entity.ProductInformation = new ProductInformation() { Price = 5000 };
// Some check that handling legacy stuff does not break things
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
paymentMethod.Calculate();
Assert.NotNull(paymentMethod);
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
////////////////////
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
@ -1128,8 +1118,6 @@ namespace BTCPayServer.Tests
var txFee = Money.Zero;
var rate = user.BitPay.GetRates();
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
@ -1233,40 +1221,52 @@ namespace BTCPayServer.Tests
[Fact]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider("BTC");
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
quadri = new QuadrigacxRateProvider("LTC");
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
[Fact]
public void CheckRatesProvider()
{
var coinAverage = new CoinAverageRateProvider("BTC");
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var coinAverage = new CoinAverageRateProvider(provider);
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();
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
var cached = new CachedRateProvider("BTC", coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
cached.CacheSpan = TimeSpan.FromSeconds(10);
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
factory.DirectProviders.Clear();
factory.CacheSpan = TimeSpan.FromSeconds(10);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Thread.Sleep(11000);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value);
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)

View File

@ -44,7 +44,6 @@ namespace BTCPayServer
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
public RateProviderDescription DefaultRateProvider { get; set; }
[Obsolete("Should not be needed")]
public bool IsBTC
@ -62,6 +61,7 @@ namespace BTCPayServer
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
public override string ToString()
{

View File

@ -14,9 +14,6 @@ namespace BTCPayServer
public void InitBitcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
var coinaverage = new CoinAverageRateProviderDescription("BTC");
var bitpay = new BitpayRateProviderDescription();
var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay });
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
@ -24,7 +21,6 @@ namespace BTCPayServer
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -20,7 +20,7 @@ namespace BTCPayServer
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" },
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")

View File

@ -20,7 +20,6 @@ namespace BTCPayServer
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -460,7 +460,7 @@ namespace BTCPayServer.Controllers
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
}
catch (RateUnavailableException)
catch (BitpayHttpException)
{
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
return View(model);

View File

@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
IRateProviderFactory _RateProviders;
BTCPayRateProviderFactory _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviders,
BTCPayRateProviderFactory rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_UserManager = userManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
@ -111,6 +112,23 @@ namespace BTCPayServer.Controllers
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
if (storeBlob.LightningMaxValue != null)
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
if (storeBlob.OnChainMinValue != null)
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
@ -119,19 +137,45 @@ namespace BTCPayServer.Controllers
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
foreach(var pair in fetchingByCurrencyPair)
{
var rateResult = await pair.Value;
bool hasError = false;
if(rateResult.Errors.Count != 0)
{
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
hasError = true;
}
if(rateResult.ExchangeExceptions.Count != 0)
{
foreach(var ex in rateResult.ExchangeExceptions)
{
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
hasError = true;
}
if(hasError)
{
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
}
}
foreach (var o in supportedPaymentMethods)
{
try
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
throw new PaymentMethodUnavailableException("Payment method unavailable");
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
@ -158,23 +202,6 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
#pragma warning disable CS0618
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
{
var btc = _NetworkProvider.BTC;
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
entity.Rate = await gettingRate;
}
#pragma warning restore CS0618
}
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
@ -183,15 +210,17 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{
var storeBlob = store.GetStoreBlob();
var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.Value == null)
return null;
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
paymentMethod.Rate = rate.Value.Value;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
@ -217,16 +246,14 @@ namespace BTCPayServer.Controllers
if (compare != null)
{
var limitValueRate = 0.0m;
if (limitValue.Currency == entity.ProductInformation.Currency)
limitValueRate = paymentMethod.Rate;
else
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.Value.HasValue)
{
throw new PaymentMethodUnavailableException(errorMessage);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
throw new PaymentMethodUnavailableException(errorMessage);
}
}
}
///////////////
@ -243,13 +270,6 @@ namespace BTCPayServer.Controllers
return paymentMethod;
}
#pragma warning disable CS0618
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
{
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
}
#pragma warning restore CS0618
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
if (transactionSpeed == null)

View File

@ -8,17 +8,19 @@ using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Rating;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
IRateProviderFactory _RateProviderFactory;
BTCPayRateProviderFactory _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public RateController(
IRateProviderFactory rateProviderFactory,
BTCPayRateProviderFactory rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
@ -32,45 +34,90 @@ namespace BTCPayServer.Controllers
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
var result = await GetRates2(cryptoCode, storeId);
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
if(rates == null)
if (rates == null)
return result;
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
{
cryptoCode = cryptoCode ?? "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
RateRules rules = null;
if (storeId != null)
if(storeId == null || currencyPairs == null)
{
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return NotFound();
rules = store.GetStoreBlob().GetRateRules();
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
result.StatusCode = 400;
return result;
}
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
if (rateProvider == null)
return NotFound();
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
result.StatusCode = 404;
return result;
}
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r =>
new NBitpayClient.Rate()
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
foreach(var currency in currencyPairs.Split(','))
{
if(!CurrencyPair.TryParse(currency, out var pair))
{
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
result.StatusCode = 400;
return result;
}
pairs.Add(pair);
}
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
.Where(r => r.Value.HasValue)
.Select(r =>
new Rate()
{
Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value
CryptoCode = r.Pair.Left,
Code = r.Pair.Right,
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
Value = r.Value.Value
}).Where(n => n.Name != null).ToArray());
}
public class Rate
{
[JsonProperty(PropertyName = "name")]
public string Name
{
get;
set;
}
[JsonProperty(PropertyName = "cryptoCode")]
public string CryptoCode
{
get;
set;
}
[JsonProperty(PropertyName = "code")]
public string Code
{
get;
set;
}
[JsonProperty(PropertyName = "rate")]
public decimal Value
{
get;
set;
}
}
}
}

View File

@ -24,10 +24,10 @@ namespace BTCPayServer.Controllers
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private BTCPayRateProviderFactory _RateProviderFactory;
public ServerController(UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviderFactory,
BTCPayRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository)
{
_UserManager = userManager;
@ -99,7 +99,7 @@ namespace BTCPayServer.Controllers
};
if (!withAuth || settings.GetCoinAverageSignature() != null)
{
return new CoinAverageRateProvider("BTC")
return new CoinAverageRateProvider()
{ Authenticator = settings };
}
return null;

View File

@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
@ -370,7 +370,7 @@ namespace BTCPayServer.Controllers
needUpdate = true;
}
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
if (newExchange)
{
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
@ -392,12 +392,12 @@ namespace BTCPayServer.Controllers
});
}
private (String DisplayName, String Name)[] GetSupportedExchanges()
private CoinAverageExchange[] GetSupportedExchanges()
{
return new[] { ("Coin Average", "coinaverage") }
.Concat(_CoinAverage.AvailableExchanges)
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
.ToArray();
return _CoinAverage.AvailableExchanges
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)

View File

@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Security;
using BTCPayServer.Rating;
namespace BTCPayServer.Data
{
@ -207,7 +208,10 @@ namespace BTCPayServer.Data
public StoreBlob GetStoreBlob()
{
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
if (result.PreferredExchange == null)
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
return result;
}
public bool SetStoreBlob(StoreBlob storeBlob)
@ -221,9 +225,9 @@ namespace BTCPayServer.Data
}
}
public class RateRule
public class RateRule_Obsolete
{
public RateRule()
public RateRule_Obsolete()
{
RuleName = "Multiplier";
}
@ -275,8 +279,8 @@ namespace BTCPayServer.Data
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule>();
RateRules.Add(new RateRule() { Multiplier = rate });
RateRules = new List<RateRule_Obsolete>();
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
@ -290,7 +294,7 @@ namespace BTCPayServer.Data
return rate;
}
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
@ -303,6 +307,10 @@ namespace BTCPayServer.Data
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomCSS { get; set; }
public bool RateScripting { get; set; }
public string RateScript { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
@ -317,12 +325,44 @@ namespace BTCPayServer.Data
}
}
public RateRules GetRateRules()
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
{
return new RateRules(RateRules)
if (!RateScripting ||
string.IsNullOrEmpty(RateScript) ||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
{
PreferredExchange = PreferredExchange
};
return GetDefaultRateRules(networkProvider);
}
else
{
rules.GlobalMultiplier = GetRateMultiplier();
return rules;
}
}
public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider)
{
StringBuilder builder = new StringBuilder();
foreach (var network in networkProvider.GetAll())
{
if (network.DefaultRateRules.Length != 0)
{
builder.AppendLine($"// Default rate rules for {network.CryptoCode}");
foreach (var line in network.DefaultRateRules)
{
builder.AppendLine(line);
}
builder.AppendLine($"////////");
builder.AppendLine();
}
}
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange;
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
rules.GlobalMultiplier = GetRateMultiplier();
return rules;
}
}
}

View File

@ -104,12 +104,6 @@ namespace BTCPayServer
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static bool IsCoinAverage(this string exchangeName)
{
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();

View File

@ -17,15 +17,15 @@ namespace BTCPayServer.HostedServices
public class RatesHostedService : BaseAsyncService
{
private SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private CoinAverageSettings _coinAverageSettings;
BTCPayRateProviderFactory _RateProviderFactory;
public RatesHostedService(SettingsRepository repo,
CoinAverageSettings coinAverageSettings,
IRateProviderFactory rateProviderFactory)
BTCPayRateProviderFactory rateProviderFactory,
CoinAverageSettings coinAverageSettings)
{
this._SettingsRepository = repo;
_RateProviderFactory = rateProviderFactory;
_coinAverageSettings = coinAverageSettings;
_RateProviderFactory = rateProviderFactory;
}
internal override Task[] InitializeTasks()
@ -40,11 +40,15 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSupportedExchanges()
{
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
_coinAverageSettings.AvailableExchanges = tickers
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach(var item in tickers
.Exchanges
.Select(c => (c.DisplayName, c.Name))
.ToArray();
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{
exchanges.Add(item);
}
_coinAverageSettings.AvailableExchanges = exchanges;
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
}

View File

@ -68,7 +68,6 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -129,7 +128,7 @@ namespace BTCPayServer.Hosting
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
services.TryAddSingleton<BTCPayRateProviderFactory>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();

View File

@ -1,5 +1,6 @@
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
@ -49,10 +50,10 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange)
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
{
var defaultStore = preferredExchange ?? "coinaverage";
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
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;
@ -67,7 +68,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
get
{
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
}

View File

@ -9,6 +9,18 @@ namespace BTCPayServer.Rating
{
public class ExchangeRates : IEnumerable<ExchangeRate>
{
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
public ExchangeRates()
{
}
public ExchangeRates(IEnumerable<ExchangeRate> rates)
{
foreach (var rate in rates)
{
Add(rate);
}
}
List<ExchangeRate> _Rates = new List<ExchangeRate>();
public MultiValueDictionary<string, ExchangeRate> ByExchange
{
@ -18,8 +30,22 @@ namespace BTCPayServer.Rating
public void Add(ExchangeRate rate)
{
_Rates.Add(rate);
ByExchange.Add(rate.Exchange, rate);
// 1 DOGE is always 1 DOGE
if (rate.CurrencyPair.Left == rate.CurrencyPair.Right)
return;
var key = $"({rate.Exchange}) {rate.CurrencyPair}";
if (_AllRates.TryAdd(key, rate))
{
_Rates.Add(rate);
ByExchange.Add(rate.Exchange, rate);
}
else
{
if (rate.Value.HasValue)
{
_AllRates[key].Value = rate.Value;
}
}
}
public IEnumerator<ExchangeRate> GetEnumerator()
@ -34,7 +60,7 @@ namespace BTCPayServer.Rating
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
{
if(ByExchange.TryGetValue(exchangeName, out var rates))
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
@ -43,6 +69,8 @@ namespace BTCPayServer.Rating
}
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
{
if (currencyPair.Left == currencyPair.Right)
return 1.0m;
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);

View File

@ -93,6 +93,8 @@ namespace BTCPayServer.Rating
SyntaxNode root;
RuleList ruleList;
public decimal GlobalMultiplier { get; set; } = 1.0m;
RateRules(SyntaxNode root)
{
ruleList = new RuleList();
@ -113,15 +115,15 @@ namespace BTCPayServer.Rating
return true;
}
public RateRule GetRuleFor(CurrencyPair currencyPair, decimal globalMultiplier = 1.0m)
public RateRule GetRuleFor(CurrencyPair currencyPair)
{
if (currencyPair.Left == "X" || currencyPair.Right == "X")
throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency");
var candidate = FindBestCandidate(currencyPair);
if (globalMultiplier != decimal.One)
if (GlobalMultiplier != decimal.One)
{
candidate = CreateExpression($"({candidate}) * {globalMultiplier.ToString(CultureInfo.InvariantCulture)}");
candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}");
}
return new RateRule(this, currencyPair, candidate);
}

View File

@ -338,14 +338,14 @@ namespace BTCPayServer.Services.Invoices
};
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
foreach (var info in this.GetPaymentMethods(networkProvider, true))
foreach (var info in this.GetPaymentMethods(networkProvider))
{
var accounting = info.Calculate();
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
cryptoInfo.Price = (accounting.TotalDue - accounting.NetworkFee).ToString();
cryptoInfo.Due = accounting.Due.ToString();
cryptoInfo.Paid = accounting.Paid.ToString();
@ -396,8 +396,7 @@ namespace BTCPayServer.Services.Invoices
dto.PaymentUrls = cryptoInfo.PaymentUrls;
}
#pragma warning restore CS0618
if (!info.IsPhantomBTC)
dto.CryptoInfo.Add(cryptoInfo);
dto.CryptoInfo.Add(cryptoInfo);
}
Populate(ProductInformation, dto);
@ -432,26 +431,15 @@ namespace BTCPayServer.Services.Invoices
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
}
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false)
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider)
{
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
var serializer = new Serializer(Dummy);
PaymentMethod phantom = null;
#pragma warning disable CS0618
// Legacy
if (alwaysIncludeBTC)
{
var btcNetwork = networkProvider?.GetNetwork("BTC");
phantom = new PaymentMethod() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork };
if (btcNetwork != null || networkProvider == null)
rates.Add(phantom);
}
if (PaymentMethod != null)
{
foreach (var prop in PaymentMethod.Properties())
{
if (prop.Name == "BTC" && phantom != null)
rates.Remove(phantom);
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
r.CryptoCode = paymentMethodId.CryptoCode;
@ -635,20 +623,17 @@ namespace BTCPayServer.Services.Invoices
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
public string DepositAddress { get; set; }
[JsonIgnore]
public bool IsPhantomBTC { get; set; }
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null)
{
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
var paymentMethods = ParentEntity.GetPaymentMethods(null, IsPhantomBTC);
var paymentMethods = ParentEntity.GetPaymentMethods(null);
var totalDue = ParentEntity.ProductInformation.Price / Rate;
var paid = 0m;
var cryptoPaid = 0.0m;
int precision = 8;
var paidTxFee = 0m;
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
int txRequired = 0;
var payments =
@ -662,9 +647,8 @@ namespace BTCPayServer.Services.Invoices
if (!paidEnough)
{
totalDue += txFee;
paidTxFee += txFee;
}
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision);
if (GetId() == _.GetPaymentMethodId())
{
cryptoPaid += _.GetCryptoPaymentData().GetValue();
@ -680,16 +664,15 @@ namespace BTCPayServer.Services.Invoices
{
txRequired++;
totalDue += GetTxFee();
paidTxFee += GetTxFee();
}
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
accounting.Paid = Money.Coins(paid);
accounting.Paid = Money.Coins(Extensions.RoundUp(paid, precision));
accounting.TxRequired = txRequired;
accounting.CryptoPaid = Money.Coins(cryptoPaid);
accounting.CryptoPaid = Money.Coins(Extensions.RoundUp(cryptoPaid, precision));
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
accounting.NetworkFee = Money.Coins(paidTxFee);
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
return accounting;
}

View File

@ -3,13 +3,35 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class BTCPayRateProviderFactory : IRateProviderFactory
public class ExchangeException
{
public Exception Exception { get; set; }
public string ExchangeName { get; set; }
}
public class RateResult
{
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public decimal? Value { get; set; }
public bool Cached { get; internal set; }
}
public class BTCPayRateProviderFactory
{
class QueryRateResult
{
public bool CachedResult { get; set; }
public List<ExchangeException> Exceptions { get; set; }
public ExchangeRates ExchangeRates { get; set; }
}
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
@ -20,18 +42,41 @@ namespace BTCPayServer.Services.Rates
return _Cache;
}
}
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, IServiceProvider serviceProvider)
CoinAverageSettings _CoinAverageSettings;
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
BTCPayNetworkProvider btcpayNetworkProvider,
CoinAverageSettings coinAverageSettings)
{
if (cacheOptions == null)
throw new ArgumentNullException(nameof(cacheOptions));
_CoinAverageSettings = coinAverageSettings;
_Cache = new MemoryCache(cacheOptions);
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
this.serviceProvider = serviceProvider;
this.btcpayNetworkProvider = btcpayNetworkProvider;
InitExchanges();
}
IServiceProvider serviceProvider;
public bool UseCoinAverageAsFallback { get; set; } = true;
private void InitExchanges()
{
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
}
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> DirectProviders
{
get
{
return _DirectProviders;
}
}
BTCPayNetworkProvider btcpayNetworkProvider;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
@ -51,45 +96,87 @@ namespace BTCPayServer.Services.Rates
_Cache = new MemoryCache(_CacheOptions);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
{
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);
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
}
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
{
if (rules == null)
throw new ArgumentNullException(nameof(rules));
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{
var dependentQueries = new List<Task<QueryRateResult>>();
foreach (var requiredExchange in i.RateRule.ExchangeRates)
{
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
{
fetching = QueryRates(requiredExchange.Exchange);
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
}
dependentQueries.Add(fetching);
}
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
}
return fetchingRates;
}
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
{
var result = new RateResult();
result.Cached = true;
foreach (var queryAsync in dependentQueries)
{
var query = await queryAsync;
if (!query.CachedResult)
result.Cached = false;
result.ExchangeExceptions.AddRange(query.Exceptions);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.Add(rule);
}
}
rateRule.Reevaluate();
result.Value = rateRule.Value;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);
return result;
}
private async Task<QueryRateResult> QueryRates(string exchangeName)
{
List<IRateProvider> providers = new List<IRateProvider>();
if(exchange == "quadrigacx")
if (DirectProviders.TryGetValue(exchangeName, out var directProvider))
providers.Add(directProvider);
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
{
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
providers.Add(new CoinAverageRateProvider(btcpayNetworkProvider)
{
Exchange = exchangeName,
Authenticator = _CoinAverageSettings
});
}
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
coinAverage.Exchange = exchange;
providers.Add(coinAverage);
return new FallbackRateProvider(providers.ToArray());
}
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)
{
if(network.DefaultRateProvider == null)
var fallback = new FallbackRateProvider(providers.ToArray());
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
{
throw new RateUnavailableException(network.CryptoCode);
}
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
CacheSpan = CacheSpan
};
var value = await cached.GetRatesAsync();
return new QueryRateResult()
{
CachedResult = !fallback.Used,
ExchangeRates = value,
Exceptions = fallback.Exceptions
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
};
}
}
}

View File

@ -5,18 +5,13 @@ using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class BitpayRateProviderDescription : RateProviderDescription
{
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
}
}
public class BitpayRateProvider : IRateProvider
{
public const string BitpayName = "bitpay";
Bitpay _Bitpay;
public BitpayRateProvider(Bitpay bitpay)
{
@ -24,21 +19,13 @@ namespace BTCPayServer.Services.Rates
throw new ArgumentNullException(nameof(bitpay));
_Bitpay = bitpay;
}
public async Task<decimal> GetRateAsync(string currency)
{
var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false);
var rate = rates.GetRate(currency);
if (rate == 0m)
throw new RateUnavailableException(currency);
return (decimal)rate;
}
public async Task<ICollection<Rate>> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync()
{
return (await _Bitpay.GetRatesAsync().ConfigureAwait(false))
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
.AllRates
.Select(r => new Rate() { Currency = r.Code, Value = r.Value })
.ToList();
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
.ToList());
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
@ -10,9 +11,8 @@ namespace BTCPayServer.Services.Rates
{
private IRateProvider _Inner;
private IMemoryCache _MemoryCache;
private string _CryptoCode;
public CachedRateProvider(string cryptoCode, IRateProvider inner, IMemoryCache memoryCache)
public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this.MemoryCache = memoryCache;
this._CryptoCode = cryptoCode;
this.ExchangeName = exchangeName;
}
public IRateProvider Inner
@ -31,31 +31,22 @@ namespace BTCPayServer.Services.Rates
}
}
public string ExchangeName { get; set; }
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
public Task<decimal> GetRateAsync(string currency)
{
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency);
});
}
public Task<ICollection<Rate>> GetRatesAsync()
public Task<ExchangeRates> GetRatesAsync()
{
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
});
}
public string AdditionalScope { get; set; }
}
}

View File

@ -10,6 +10,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
@ -21,29 +22,6 @@ namespace BTCPayServer.Services.Rates
}
}
public class CoinAverageRateProviderDescription : RateProviderDescription
{
public CoinAverageRateProviderDescription(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new CoinAverageRateProvider(CryptoCode)
{
Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>()
};
}
IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider)
{
return CreateRateProvider(serviceProvider);
}
}
public class GetExchangeTickersResponse
{
public class Exchange
@ -69,18 +47,25 @@ namespace BTCPayServer.Services.Rates
public interface ICoinAverageAuthenticator
{
Task AddHeader(HttpRequestMessage message);
}
}
public class CoinAverageRateProvider : IRateProvider
{
public const string CoinAverageName = "coinaverage";
BTCPayNetworkProvider _NetworkProvider;
public CoinAverageRateProvider()
{
}
public CoinAverageRateProvider(BTCPayNetworkProvider networkProvider)
{
if (networkProvider == null)
throw new ArgumentNullException(nameof(networkProvider));
_NetworkProvider = networkProvider;
}
static HttpClient _Client = new HttpClient();
public CoinAverageRateProvider(string cryptoCode)
{
CryptoCode = cryptoCode ?? "BTC";
}
public string Exchange { get; set; }
public string Exchange { get; set; } = CoinAverageName;
public string CryptoCode { get; set; }
@ -88,27 +73,19 @@ namespace BTCPayServer.Services.Rates
{
get; set;
} = "global";
public async Task<decimal> GetRateAsync(string currency)
{
var rates = await GetRatesCore();
return GetRate(rates, currency);
}
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
{
if (currency == "BTC")
return 1.0m;
if (rates.TryGetValue(currency, out decimal result))
return result;
throw new RateUnavailableException(currency);
}
public ICoinAverageAuthenticator Authenticator { get; set; }
private async Task<Dictionary<string, decimal>> GetRatesCore()
private bool TryToDecimal(JProperty p, out decimal v)
{
string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ExchangeRates> GetRatesAsync()
{
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var auth = Authenticator;
@ -128,36 +105,34 @@ namespace BTCPayServer.Services.Rates
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
if(Exchange != null)
if (Exchange != CoinAverageName)
{
rates = (JObject)rates["symbols"];
}
return rates.Properties()
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused))
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
{
TryToDecimal(p, out decimal v);
return v;
});
var exchangeRates = new ExchangeRates();
foreach (var prop in rates.Properties())
{
ExchangeRate exchangeRate = new ExchangeRate();
exchangeRate.Exchange = Exchange;
if (!TryToDecimal(prop, out decimal value))
continue;
exchangeRate.Value = value;
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = prop.Name.Substring(0, i);
if (_NetworkProvider.GetNetwork(potentialCryptoName) != null)
{
exchangeRate.CurrencyPair = new CurrencyPair(potentialCryptoName, prop.Name.Substring(i));
}
}
if (exchangeRate.CurrencyPair != null)
exchangeRates.Add(exchangeRate);
}
return exchangeRates;
}
}
private bool TryToDecimal(JProperty p, out decimal v)
{
JToken token = p.Value[Exchange == null ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
var rates = await GetRatesCore();
return rates.Select(o => new Rate()
{
Currency = o.Key,
Value = o.Value
}).ToList();
}
public async Task TestAuthAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
@ -217,7 +192,7 @@ namespace BTCPayServer.Services.Rates
var exchanges = (JObject)jobj["exchanges"];
response.Exchanges = exchanges
.Properties()
.Select(p =>
.Select(p =>
{
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
exchange.Name = p.Name;

View File

@ -20,12 +20,35 @@ namespace BTCPayServer.Services.Rates
return _Settings.AddHeader(message);
}
}
public class CoinAverageExchange
{
public CoinAverageExchange(string name, string display)
{
Name = name;
Display = display;
}
public string Name { get; set; }
public string Display { get; set; }
}
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
{
public CoinAverageExchanges()
{
Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
}
public void Add(CoinAverageExchange exchange)
{
Add(exchange.Name, exchange);
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
{
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges();
public CoinAverageSettings()
{
@ -37,8 +60,9 @@ namespace BTCPayServer.Services.Rates
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new[] {
AvailableExchanges = new CoinAverageExchanges();
foreach(var item in
new[] {
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
@ -89,7 +113,10 @@ namespace BTCPayServer.Services.Rates
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Bitso", Name: "bitso"),
}.ToArray();
})
{
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
}
}
public Task AddHeader(HttpRequestMessage message)

View File

@ -2,58 +2,35 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class FallbackRateProviderDescription : RateProviderDescription
{
public FallbackRateProviderDescription(RateProviderDescription[] rateProviders)
{
RateProviders = rateProviders;
}
public RateProviderDescription[] RateProviders { get; set; }
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new FallbackRateProvider(RateProviders.Select(r => r.CreateRateProvider(serviceProvider)).ToArray());
}
}
public class FallbackRateProvider : IRateProvider
{
IRateProvider[] _Providers;
public bool Used { get; set; }
public FallbackRateProvider(IRateProvider[] providers)
{
if (providers == null)
throw new ArgumentNullException(nameof(providers));
_Providers = providers;
}
public async Task<decimal> GetRateAsync(string currency)
{
foreach(var p in _Providers)
{
try
{
return await p.GetRateAsync(currency).ConfigureAwait(false);
}
catch { }
}
throw new RateUnavailableException(currency);
}
public async Task<ICollection<Rate>> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync()
{
Used = true;
foreach (var p in _Providers)
{
try
{
return await p.GetRatesAsync().ConfigureAwait(false);
}
catch { }
catch(Exception ex) { Exceptions.Add(ex); }
}
throw new RateUnavailableException("ALL");
return new ExchangeRates();
}
public List<Exception> Exceptions { get; set; } = new List<Exception>();
}
}

View File

@ -2,32 +2,12 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class Rate
{
public Rate()
{
}
public Rate(string currency, decimal value)
{
Value = value;
Currency = currency;
}
public string Currency
{
get; set;
}
public decimal Value
{
get; set;
}
}
public interface IRateProvider
{
Task<decimal> GetRateAsync(string currency);
Task<ICollection<Rate>> GetRatesAsync();
Task<ExchangeRates> GetRatesAsync();
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
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
{
IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules);
TimeSpan CacheSpan { get; set; }
void InvalidateCache();
}
}

View File

@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class MockRateProviderFactory : IRateProviderFactory
{
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
public MockRateProviderFactory()
{
}
public TimeSpan CacheSpan { get; set; }
public void AddMock(MockRateProvider mock)
{
_Mocks.Add(mock);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
{
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
}
public void InvalidateCache()
{
}
}
public class MockRateProvider : IRateProvider
{
List<Rate> _Rates;
public string CryptoCode { get; }
public MockRateProvider(string cryptoCode, params Rate[] rates)
{
_Rates = new List<Rate>(rates);
CryptoCode = cryptoCode;
}
public MockRateProvider(string cryptoCode, List<Rate> rates)
{
_Rates = rates;
CryptoCode = cryptoCode;
}
public Task<decimal> GetRateAsync(string currency)
{
var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase));
if (rate == null)
throw new RateUnavailableException(currency);
return Task.FromResult(rate.Value);
}
public Task<ICollection<Rate>> GetRatesAsync()
{
ICollection<Rate> rates = _Rates;
return Task.FromResult(rates);
}
}
}

View File

@ -4,32 +4,15 @@ using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class QuadrigacxRateProvider : IRateProvider
{
public QuadrigacxRateProvider(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
public const string QuadrigacxName = "quadrigacx";
static HttpClient _Client = new HttpClient();
public async Task<decimal> GetRateAsync(string currency)
{
return await GetRatesAsyncCore(CryptoCode, currency);
}
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
if (!TryToDecimal(rates, out var result))
throw new RateUnavailableException(currency);
return result;
}
private bool TryToDecimal(JObject p, out decimal v)
{
@ -40,26 +23,26 @@ namespace BTCPayServer.Services.Rates
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync()
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
List<Rate> result = new List<Rate>();
var exchangeRates = new ExchangeRates();
foreach (var prop in rates.Properties())
{
var rate = new Rate();
var splitted = prop.Name.Split('_');
var crypto = splitted[0].ToUpperInvariant();
if (crypto != CryptoCode)
var rate = new ExchangeRate();
if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair))
continue;
rate.CurrencyPair = pair;
rate.Exchange = QuadrigacxName;
if (!TryToDecimal((JObject)prop.Value, out var v))
continue;
rate.Currency = splitted[1].ToUpperInvariant();
TryToDecimal((JObject)prop.Value, out var v);
rate.Value = v;
result.Add(rate);
exchangeRates.Add(rate);
}
return result;
return exchangeRates;
}
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public interface RateProviderDescription
{
IRateProvider CreateRateProvider(IServiceProvider serviceProvider);
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Services.Rates
{
public class RateUnavailableException : Exception
{
public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency)
{
if (currency == null)
throw new ArgumentNullException(nameof(currency));
Currency = currency;
}
public string Currency
{
get; set;
}
}
}

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Services.Rates
{
public class TweakRateProvider : IRateProvider
{
private BTCPayNetwork network;
private IRateProvider rateProvider;
private RateRules rateRules;
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (rateProvider == null)
throw new ArgumentNullException(nameof(rateProvider));
if (rateRules == null)
throw new ArgumentNullException(nameof(rateRules));
this.network = network;
this.rateProvider = rateProvider;
this.rateRules = rateRules;
}
public async Task<decimal> GetRateAsync(string currency)
{
var rate = await rateProvider.GetRateAsync(currency);
foreach(var rule in rateRules)
{
rate = rule.Apply(network, rate);
}
return rate;
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
List<Rate> rates = new List<Rate>();
foreach (var rate in await rateProvider.GetRatesAsync())
{
var localRate = rate.Value;
foreach (var rule in rateRules)
{
localRate = rule.Apply(network, localRate);
}
rates.Add(new Rate(rate.Currency, localRate));
}
return rates;
}
}
}