mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Refactor the RateProvider
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
using BTCPayServer.Hosting;
|
using BTCPayServer.Hosting;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
@@ -106,15 +107,6 @@ namespace BTCPayServer.Tests
|
|||||||
.UseConfiguration(conf)
|
.UseConfiguration(conf)
|
||||||
.ConfigureServices(s =>
|
.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 =>
|
s.AddLogging(l =>
|
||||||
{
|
{
|
||||||
l.SetMinimumLevel(LogLevel.Information)
|
l.SetMinimumLevel(LogLevel.Information)
|
||||||
@@ -128,6 +120,30 @@ namespace BTCPayServer.Tests
|
|||||||
.Build();
|
.Build();
|
||||||
_Host.Start();
|
_Host.Start();
|
||||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
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
|
public string HostName
|
||||||
|
|||||||
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,8 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
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
|
// Check errors conditions
|
||||||
@@ -107,7 +108,8 @@ namespace BTCPayServer.Tests
|
|||||||
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||||
builder.AppendLine("DOGE_BTC = 2000");
|
builder.AppendLine("DOGE_BTC = 2000");
|
||||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
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());
|
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
|
||||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
|
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
|
||||||
Assert.True(rule2.Reevaluate());
|
Assert.True(rule2.Reevaluate());
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ using BTCPayServer.Services.Apps;
|
|||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -108,22 +109,11 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
var entity = new InvoiceEntity();
|
var entity = new InvoiceEntity();
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
entity.TxFee = Money.Coins(0.1m);
|
|
||||||
entity.Rate = 5000;
|
|
||||||
|
|
||||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
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 };
|
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||||
|
|
||||||
// Some check that handling legacy stuff does not break things
|
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
|
||||||
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 accounting = paymentMethod.Calculate();
|
var accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||||
@@ -1128,8 +1118,6 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
var txFee = Money.Zero;
|
var txFee = Money.Zero;
|
||||||
|
|
||||||
var rate = user.BitPay.GetRates();
|
|
||||||
|
|
||||||
var cashCow = tester.ExplorerNode;
|
var cashCow = tester.ExplorerNode;
|
||||||
|
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||||
@@ -1233,40 +1221,52 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CheckQuadrigacxRateProvider()
|
public void CheckQuadrigacxRateProvider()
|
||||||
{
|
{
|
||||||
var quadri = new QuadrigacxRateProvider("BTC");
|
var quadri = new QuadrigacxRateProvider();
|
||||||
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||||
Assert.NotEmpty(rates);
|
Assert.NotEmpty(rates);
|
||||||
Assert.NotEqual(0.0m, rates.First().Value);
|
Assert.NotEqual(0.0m, rates.First().Value);
|
||||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
|
||||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
|
||||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
|
||||||
|
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CheckRatesProvider()
|
public void CheckRatesProvider()
|
||||||
{
|
{
|
||||||
var coinAverage = new CoinAverageRateProvider("BTC");
|
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||||
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
|
var coinAverage = new CoinAverageRateProvider(provider);
|
||||||
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
|
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) }));
|
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
|
||||||
cached.CacheSpan = TimeSpan.FromSeconds(10);
|
|
||||||
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
var factory = new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
|
||||||
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
factory.DirectProviders.Clear();
|
||||||
//Manually check that cache get hit after 10 sec
|
factory.CacheSpan = TimeSpan.FromSeconds(10);
|
||||||
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
|
||||||
|
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)
|
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ namespace BTCPayServer
|
|||||||
public string CryptoCode { get; internal set; }
|
public string CryptoCode { get; internal set; }
|
||||||
public string BlockExplorerLink { get; internal set; }
|
public string BlockExplorerLink { get; internal set; }
|
||||||
public string UriScheme { get; internal set; }
|
public string UriScheme { get; internal set; }
|
||||||
public RateProviderDescription DefaultRateProvider { get; set; }
|
|
||||||
|
|
||||||
[Obsolete("Should not be needed")]
|
[Obsolete("Should not be needed")]
|
||||||
public bool IsBTC
|
public bool IsBTC
|
||||||
@@ -62,6 +61,7 @@ namespace BTCPayServer
|
|||||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||||
public KeyPath CoinType { get; internal set; }
|
public KeyPath CoinType { get; internal set; }
|
||||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||||
|
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ namespace BTCPayServer
|
|||||||
public void InitBitcoin()
|
public void InitBitcoin()
|
||||||
{
|
{
|
||||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
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()
|
Add(new BTCPayNetwork()
|
||||||
{
|
{
|
||||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||||
@@ -24,7 +21,6 @@ namespace BTCPayServer
|
|||||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
UriScheme = "bitcoin",
|
UriScheme = "bitcoin",
|
||||||
DefaultRateProvider = btcRate,
|
|
||||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace BTCPayServer
|
|||||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
UriScheme = "dogecoin",
|
UriScheme = "dogecoin",
|
||||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" },
|
||||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace BTCPayServer
|
|||||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
UriScheme = "litecoin",
|
UriScheme = "litecoin",
|
||||||
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
|
|
||||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ namespace BTCPayServer.Controllers
|
|||||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||||
return RedirectToAction(nameof(ListInvoices));
|
return RedirectToAction(nameof(ListInvoices));
|
||||||
}
|
}
|
||||||
catch (RateUnavailableException)
|
catch (BitpayHttpException)
|
||||||
{
|
{
|
||||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
||||||
return View(model);
|
return View(model);
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy;
|
|||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
public partial class InvoiceController : Controller
|
public partial class InvoiceController : Controller
|
||||||
{
|
{
|
||||||
InvoiceRepository _InvoiceRepository;
|
InvoiceRepository _InvoiceRepository;
|
||||||
IRateProviderFactory _RateProviders;
|
BTCPayRateProviderFactory _RateProvider;
|
||||||
StoreRepository _StoreRepository;
|
StoreRepository _StoreRepository;
|
||||||
UserManager<ApplicationUser> _UserManager;
|
UserManager<ApplicationUser> _UserManager;
|
||||||
private CurrencyNameTable _CurrencyNameTable;
|
private CurrencyNameTable _CurrencyNameTable;
|
||||||
@@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers
|
|||||||
InvoiceRepository invoiceRepository,
|
InvoiceRepository invoiceRepository,
|
||||||
CurrencyNameTable currencyNameTable,
|
CurrencyNameTable currencyNameTable,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
IRateProviderFactory rateProviders,
|
BTCPayRateProviderFactory rateProvider,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
BTCPayWalletProvider walletProvider,
|
BTCPayWalletProvider walletProvider,
|
||||||
@@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
|
|||||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
_EventAggregator = eventAggregator;
|
_EventAggregator = eventAggregator;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
@@ -111,6 +112,23 @@ namespace BTCPayServer.Controllers
|
|||||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
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)
|
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
.Select(c =>
|
.Select(c =>
|
||||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||||
@@ -119,19 +137,45 @@ namespace BTCPayServer.Controllers
|
|||||||
.Where(c => c.Network != null)
|
.Where(c => c.Network != null)
|
||||||
.Select(o =>
|
.Select(o =>
|
||||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
(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();
|
.ToList();
|
||||||
|
|
||||||
List<string> paymentMethodErrors = new List<string>();
|
List<string> paymentMethodErrors = new List<string>();
|
||||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||||
var paymentMethods = new PaymentMethodDictionary();
|
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)
|
foreach (var o in supportedPaymentMethods)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var paymentMethod = await o.PaymentMethod;
|
var paymentMethod = await o.PaymentMethod;
|
||||||
if (paymentMethod == null)
|
if (paymentMethod == null)
|
||||||
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
|
throw new PaymentMethodUnavailableException("Payment method unavailable");
|
||||||
supported.Add(o.SupportedPaymentMethod);
|
supported.Add(o.SupportedPaymentMethod);
|
||||||
paymentMethods.Add(paymentMethod);
|
paymentMethods.Add(paymentMethod);
|
||||||
}
|
}
|
||||||
@@ -158,23 +202,6 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
entity.SetSupportedPaymentMethods(supported);
|
entity.SetSupportedPaymentMethods(supported);
|
||||||
entity.SetPaymentMethods(paymentMethods);
|
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.PosData = invoice.PosData;
|
||||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||||
|
|
||||||
@@ -183,15 +210,17 @@ namespace BTCPayServer.Controllers
|
|||||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
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 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 paymentMethod = new PaymentMethod();
|
||||||
paymentMethod.ParentEntity = entity;
|
paymentMethod.ParentEntity = entity;
|
||||||
paymentMethod.Network = network;
|
paymentMethod.Network = network;
|
||||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||||
paymentMethod.Rate = rate;
|
paymentMethod.Rate = rate.Value.Value;
|
||||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
|
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
|
||||||
if (storeBlob.NetworkFeeDisabled)
|
if (storeBlob.NetworkFeeDisabled)
|
||||||
paymentDetails.SetNoTxFee();
|
paymentDetails.SetNoTxFee();
|
||||||
@@ -217,18 +246,16 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
if (compare != null)
|
if (compare != null)
|
||||||
{
|
{
|
||||||
var limitValueRate = 0.0m;
|
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
|
||||||
if (limitValue.Currency == entity.ProductInformation.Currency)
|
if (limitValueRate.Value.HasValue)
|
||||||
limitValueRate = paymentMethod.Rate;
|
{
|
||||||
else
|
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
|
||||||
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
|
|
||||||
|
|
||||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
|
|
||||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException(errorMessage);
|
throw new PaymentMethodUnavailableException(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
///////////////
|
///////////////
|
||||||
|
|
||||||
|
|
||||||
@@ -243,13 +270,6 @@ namespace BTCPayServer.Controllers
|
|||||||
return paymentMethod;
|
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)
|
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||||
{
|
{
|
||||||
if (transactionSpeed == null)
|
if (transactionSpeed == null)
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Filters;
|
using BTCPayServer.Filters;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
public class RateController : Controller
|
public class RateController : Controller
|
||||||
{
|
{
|
||||||
IRateProviderFactory _RateProviderFactory;
|
BTCPayRateProviderFactory _RateProviderFactory;
|
||||||
BTCPayNetworkProvider _NetworkProvider;
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
CurrencyNameTable _CurrencyNameTable;
|
CurrencyNameTable _CurrencyNameTable;
|
||||||
StoreRepository _StoreRepo;
|
StoreRepository _StoreRepo;
|
||||||
public RateController(
|
public RateController(
|
||||||
IRateProviderFactory rateProviderFactory,
|
BTCPayRateProviderFactory rateProviderFactory,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
StoreRepository storeRepo,
|
StoreRepository storeRepo,
|
||||||
CurrencyNameTable currencyNameTable)
|
CurrencyNameTable currencyNameTable)
|
||||||
@@ -32,45 +34,90 @@ namespace BTCPayServer.Controllers
|
|||||||
[Route("rates")]
|
[Route("rates")]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[BitpayAPIConstraint]
|
[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[];
|
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||||
if(rates == null)
|
if (rates == null)
|
||||||
return result;
|
return result;
|
||||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("api/rates")]
|
[Route("api/rates")]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||||
{
|
{
|
||||||
cryptoCode = cryptoCode ?? "BTC";
|
if(storeId == null || currencyPairs == null)
|
||||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
|
||||||
if (network == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
RateRules rules = null;
|
|
||||||
if (storeId != null)
|
|
||||||
{
|
{
|
||||||
var store = await _StoreRepo.FindStore(storeId);
|
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||||
if (store == null)
|
result.StatusCode = 400;
|
||||||
return NotFound();
|
return result;
|
||||||
rules = store.GetStoreBlob().GetRateRules();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
|
|
||||||
if (rateProvider == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var allRates = (await rateProvider.GetRatesAsync());
|
var store = await _StoreRepo.FindStore(storeId);
|
||||||
return Json(allRates.Select(r =>
|
if (store == null)
|
||||||
new NBitpayClient.Rate()
|
|
||||||
{
|
{
|
||||||
Code = r.Currency,
|
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
result.StatusCode = 404;
|
||||||
Value = r.Value
|
return result;
|
||||||
|
}
|
||||||
|
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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());
|
}).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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
private UserManager<ApplicationUser> _UserManager;
|
private UserManager<ApplicationUser> _UserManager;
|
||||||
SettingsRepository _SettingsRepository;
|
SettingsRepository _SettingsRepository;
|
||||||
private IRateProviderFactory _RateProviderFactory;
|
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||||
|
|
||||||
public ServerController(UserManager<ApplicationUser> userManager,
|
public ServerController(UserManager<ApplicationUser> userManager,
|
||||||
IRateProviderFactory rateProviderFactory,
|
BTCPayRateProviderFactory rateProviderFactory,
|
||||||
SettingsRepository settingsRepository)
|
SettingsRepository settingsRepository)
|
||||||
{
|
{
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
@@ -99,7 +99,7 @@ namespace BTCPayServer.Controllers
|
|||||||
};
|
};
|
||||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||||
{
|
{
|
||||||
return new CoinAverageRateProvider("BTC")
|
return new CoinAverageRateProvider()
|
||||||
{ Authenticator = settings };
|
{ Authenticator = settings };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
var vm = new StoreViewModel();
|
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.Id = store.Id;
|
||||||
vm.StoreName = store.StoreName;
|
vm.StoreName = store.StoreName;
|
||||||
vm.StoreWebsite = store.StoreWebsite;
|
vm.StoreWebsite = store.StoreWebsite;
|
||||||
@@ -370,7 +370,7 @@ namespace BTCPayServer.Controllers
|
|||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
if (newExchange)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||||
@@ -392,11 +392,11 @@ namespace BTCPayServer.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private (String DisplayName, String Name)[] GetSupportedExchanges()
|
private CoinAverageExchange[] GetSupportedExchanges()
|
||||||
{
|
{
|
||||||
return new[] { ("Coin Average", "coinaverage") }
|
return _CoinAverage.AvailableExchanges
|
||||||
.Concat(_CoinAverage.AvailableExchanges)
|
.Select(c => c.Value)
|
||||||
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
@@ -207,7 +208,10 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public StoreBlob GetStoreBlob()
|
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)
|
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";
|
RuleName = "Multiplier";
|
||||||
}
|
}
|
||||||
@@ -275,8 +279,8 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public void SetRateMultiplier(double rate)
|
public void SetRateMultiplier(double rate)
|
||||||
{
|
{
|
||||||
RateRules = new List<RateRule>();
|
RateRules = new List<RateRule_Obsolete>();
|
||||||
RateRules.Add(new RateRule() { Multiplier = rate });
|
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
|
||||||
}
|
}
|
||||||
public decimal GetRateMultiplier()
|
public decimal GetRateMultiplier()
|
||||||
{
|
{
|
||||||
@@ -290,7 +294,7 @@ namespace BTCPayServer.Data
|
|||||||
return rate;
|
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; }
|
public string PreferredExchange { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||||
@@ -303,6 +307,10 @@ namespace BTCPayServer.Data
|
|||||||
[JsonConverter(typeof(UriJsonConverter))]
|
[JsonConverter(typeof(UriJsonConverter))]
|
||||||
public Uri CustomCSS { get; set; }
|
public Uri CustomCSS { get; set; }
|
||||||
|
|
||||||
|
public bool RateScripting { get; set; }
|
||||||
|
|
||||||
|
public string RateScript { get; set; }
|
||||||
|
|
||||||
|
|
||||||
string _LightningDescriptionTemplate;
|
string _LightningDescriptionTemplate;
|
||||||
public 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,12 +104,6 @@ namespace BTCPayServer
|
|||||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
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))
|
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||||
{
|
{
|
||||||
hashes = hashes.Distinct().ToArray();
|
hashes = hashes.Distinct().ToArray();
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ namespace BTCPayServer.HostedServices
|
|||||||
public class RatesHostedService : BaseAsyncService
|
public class RatesHostedService : BaseAsyncService
|
||||||
{
|
{
|
||||||
private SettingsRepository _SettingsRepository;
|
private SettingsRepository _SettingsRepository;
|
||||||
private IRateProviderFactory _RateProviderFactory;
|
|
||||||
private CoinAverageSettings _coinAverageSettings;
|
private CoinAverageSettings _coinAverageSettings;
|
||||||
|
BTCPayRateProviderFactory _RateProviderFactory;
|
||||||
public RatesHostedService(SettingsRepository repo,
|
public RatesHostedService(SettingsRepository repo,
|
||||||
CoinAverageSettings coinAverageSettings,
|
BTCPayRateProviderFactory rateProviderFactory,
|
||||||
IRateProviderFactory rateProviderFactory)
|
CoinAverageSettings coinAverageSettings)
|
||||||
{
|
{
|
||||||
this._SettingsRepository = repo;
|
this._SettingsRepository = repo;
|
||||||
_RateProviderFactory = rateProviderFactory;
|
|
||||||
_coinAverageSettings = coinAverageSettings;
|
_coinAverageSettings = coinAverageSettings;
|
||||||
|
_RateProviderFactory = rateProviderFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override Task[] InitializeTasks()
|
internal override Task[] InitializeTasks()
|
||||||
@@ -40,11 +40,15 @@ namespace BTCPayServer.HostedServices
|
|||||||
async Task RefreshCoinAverageSupportedExchanges()
|
async Task RefreshCoinAverageSupportedExchanges()
|
||||||
{
|
{
|
||||||
await new SynchronizationContextRemover();
|
await new SynchronizationContextRemover();
|
||||||
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||||
_coinAverageSettings.AvailableExchanges = tickers
|
var exchanges = new CoinAverageExchanges();
|
||||||
|
foreach(var item in tickers
|
||||||
.Exchanges
|
.Exchanges
|
||||||
.Select(c => (c.DisplayName, c.Name))
|
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||||
.ToArray();
|
{
|
||||||
|
exchanges.Add(item);
|
||||||
|
}
|
||||||
|
_coinAverageSettings.AvailableExchanges = exchanges;
|
||||||
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<TokenRepository>();
|
services.TryAddSingleton<TokenRepository>();
|
||||||
services.TryAddSingleton<EventAggregator>();
|
services.TryAddSingleton<EventAggregator>();
|
||||||
services.TryAddSingleton<CoinAverageSettings>();
|
services.TryAddSingleton<CoinAverageSettings>();
|
||||||
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
|
|
||||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||||
{
|
{
|
||||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||||
@@ -129,7 +128,7 @@ namespace BTCPayServer.Hosting
|
|||||||
else
|
else
|
||||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||||
});
|
});
|
||||||
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
|
services.TryAddSingleton<BTCPayRateProviderFactory>();
|
||||||
|
|
||||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
services.AddTransient<AccessTokenController>();
|
services.AddTransient<AccessTokenController>();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Validations;
|
using BTCPayServer.Validations;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using System;
|
using System;
|
||||||
@@ -49,10 +50,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
|
|
||||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
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 defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||||
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
|
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||||
PreferredExchange = chosen.Value;
|
PreferredExchange = chosen.Value;
|
||||||
@@ -67,7 +68,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
get
|
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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ namespace BTCPayServer.Rating
|
|||||||
{
|
{
|
||||||
public class ExchangeRates : IEnumerable<ExchangeRate>
|
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>();
|
List<ExchangeRate> _Rates = new List<ExchangeRate>();
|
||||||
public MultiValueDictionary<string, ExchangeRate> ByExchange
|
public MultiValueDictionary<string, ExchangeRate> ByExchange
|
||||||
{
|
{
|
||||||
@@ -17,10 +29,24 @@ namespace BTCPayServer.Rating
|
|||||||
} = new MultiValueDictionary<string, ExchangeRate>();
|
} = new MultiValueDictionary<string, ExchangeRate>();
|
||||||
|
|
||||||
public void Add(ExchangeRate rate)
|
public void Add(ExchangeRate 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);
|
_Rates.Add(rate);
|
||||||
ByExchange.Add(rate.Exchange, rate);
|
ByExchange.Add(rate.Exchange, rate);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (rate.Value.HasValue)
|
||||||
|
{
|
||||||
|
_AllRates[key].Value = rate.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerator<ExchangeRate> GetEnumerator()
|
public IEnumerator<ExchangeRate> GetEnumerator()
|
||||||
{
|
{
|
||||||
@@ -34,7 +60,7 @@ namespace BTCPayServer.Rating
|
|||||||
|
|
||||||
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
|
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);
|
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||||
if (rate != null)
|
if (rate != null)
|
||||||
@@ -43,6 +69,8 @@ namespace BTCPayServer.Rating
|
|||||||
}
|
}
|
||||||
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
|
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
|
||||||
{
|
{
|
||||||
|
if (currencyPair.Left == currencyPair.Right)
|
||||||
|
return 1.0m;
|
||||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||||
{
|
{
|
||||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ namespace BTCPayServer.Rating
|
|||||||
SyntaxNode root;
|
SyntaxNode root;
|
||||||
RuleList ruleList;
|
RuleList ruleList;
|
||||||
|
|
||||||
|
public decimal GlobalMultiplier { get; set; } = 1.0m;
|
||||||
|
|
||||||
RateRules(SyntaxNode root)
|
RateRules(SyntaxNode root)
|
||||||
{
|
{
|
||||||
ruleList = new RuleList();
|
ruleList = new RuleList();
|
||||||
@@ -113,15 +115,15 @@ namespace BTCPayServer.Rating
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RateRule GetRuleFor(CurrencyPair currencyPair, decimal globalMultiplier = 1.0m)
|
public RateRule GetRuleFor(CurrencyPair currencyPair)
|
||||||
{
|
{
|
||||||
if (currencyPair.Left == "X" || currencyPair.Right == "X")
|
if (currencyPair.Left == "X" || currencyPair.Right == "X")
|
||||||
throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency");
|
throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency");
|
||||||
var candidate = FindBestCandidate(currencyPair);
|
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);
|
return new RateRule(this, currencyPair, candidate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,14 +338,14 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
};
|
};
|
||||||
|
|
||||||
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
|
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 accounting = info.Calculate();
|
||||||
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
|
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
|
||||||
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
|
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
|
||||||
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
|
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
|
||||||
cryptoInfo.Rate = info.Rate;
|
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.Due = accounting.Due.ToString();
|
||||||
cryptoInfo.Paid = accounting.Paid.ToString();
|
cryptoInfo.Paid = accounting.Paid.ToString();
|
||||||
@@ -396,7 +396,6 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
dto.PaymentUrls = cryptoInfo.PaymentUrls;
|
dto.PaymentUrls = cryptoInfo.PaymentUrls;
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
if (!info.IsPhantomBTC)
|
|
||||||
dto.CryptoInfo.Add(cryptoInfo);
|
dto.CryptoInfo.Add(cryptoInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,26 +431,15 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
|
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);
|
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
|
||||||
var serializer = new Serializer(Dummy);
|
var serializer = new Serializer(Dummy);
|
||||||
PaymentMethod phantom = null;
|
|
||||||
#pragma warning disable CS0618
|
#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)
|
if (PaymentMethod != null)
|
||||||
{
|
{
|
||||||
foreach (var prop in PaymentMethod.Properties())
|
foreach (var prop in PaymentMethod.Properties())
|
||||||
{
|
{
|
||||||
if (prop.Name == "BTC" && phantom != null)
|
|
||||||
rates.Remove(phantom);
|
|
||||||
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
|
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
|
||||||
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
|
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
|
||||||
r.CryptoCode = paymentMethodId.CryptoCode;
|
r.CryptoCode = paymentMethodId.CryptoCode;
|
||||||
@@ -635,20 +623,17 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
||||||
public string DepositAddress { get; set; }
|
public string DepositAddress { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public bool IsPhantomBTC { get; set; }
|
|
||||||
|
|
||||||
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null)
|
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null)
|
||||||
{
|
{
|
||||||
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
|
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 totalDue = ParentEntity.ProductInformation.Price / Rate;
|
||||||
var paid = 0m;
|
var paid = 0m;
|
||||||
var cryptoPaid = 0.0m;
|
var cryptoPaid = 0.0m;
|
||||||
|
|
||||||
int precision = 8;
|
int precision = 8;
|
||||||
var paidTxFee = 0m;
|
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||||
int txRequired = 0;
|
int txRequired = 0;
|
||||||
var payments =
|
var payments =
|
||||||
@@ -662,9 +647,8 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
if (!paidEnough)
|
if (!paidEnough)
|
||||||
{
|
{
|
||||||
totalDue += txFee;
|
totalDue += txFee;
|
||||||
paidTxFee += txFee;
|
|
||||||
}
|
}
|
||||||
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
|
paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision);
|
||||||
if (GetId() == _.GetPaymentMethodId())
|
if (GetId() == _.GetPaymentMethodId())
|
||||||
{
|
{
|
||||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||||
@@ -680,16 +664,15 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
{
|
{
|
||||||
txRequired++;
|
txRequired++;
|
||||||
totalDue += GetTxFee();
|
totalDue += GetTxFee();
|
||||||
paidTxFee += GetTxFee();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
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.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.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||||
accounting.NetworkFee = Money.Coins(paidTxFee);
|
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||||
return accounting;
|
return accounting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,35 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
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;
|
IMemoryCache _Cache;
|
||||||
private IOptions<MemoryCacheOptions> _CacheOptions;
|
private IOptions<MemoryCacheOptions> _CacheOptions;
|
||||||
|
|
||||||
@@ -20,18 +42,41 @@ namespace BTCPayServer.Services.Rates
|
|||||||
return _Cache;
|
return _Cache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, IServiceProvider serviceProvider)
|
CoinAverageSettings _CoinAverageSettings;
|
||||||
|
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
|
||||||
|
BTCPayNetworkProvider btcpayNetworkProvider,
|
||||||
|
CoinAverageSettings coinAverageSettings)
|
||||||
{
|
{
|
||||||
if (cacheOptions == null)
|
if (cacheOptions == null)
|
||||||
throw new ArgumentNullException(nameof(cacheOptions));
|
throw new ArgumentNullException(nameof(cacheOptions));
|
||||||
|
_CoinAverageSettings = coinAverageSettings;
|
||||||
_Cache = new MemoryCache(cacheOptions);
|
_Cache = new MemoryCache(cacheOptions);
|
||||||
_CacheOptions = cacheOptions;
|
_CacheOptions = cacheOptions;
|
||||||
// We use 15 min because of limits with free version of bitcoinaverage
|
// We use 15 min because of limits with free version of bitcoinaverage
|
||||||
CacheSpan = TimeSpan.FromMinutes(15.0);
|
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;
|
TimeSpan _CacheSpan;
|
||||||
public TimeSpan CacheSpan
|
public TimeSpan CacheSpan
|
||||||
{
|
{
|
||||||
@@ -51,45 +96,87 @@ namespace BTCPayServer.Services.Rates
|
|||||||
_Cache = new MemoryCache(_CacheOptions);
|
_Cache = new MemoryCache(_CacheOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
|
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
|
||||||
{
|
{
|
||||||
rules = rules ?? new RateRules();
|
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
|
||||||
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)
|
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>();
|
List<IRateProvider> providers = new List<IRateProvider>();
|
||||||
|
if (DirectProviders.TryGetValue(exchangeName, out var directProvider))
|
||||||
if(exchange == "quadrigacx")
|
providers.Add(directProvider);
|
||||||
|
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
|
||||||
{
|
{
|
||||||
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
|
providers.Add(new CoinAverageRateProvider(btcpayNetworkProvider)
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
Exchange = exchangeName,
|
||||||
|
Authenticator = _CoinAverageSettings
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
var fallback = new FallbackRateProvider(providers.ToArray());
|
||||||
private IRateProvider GetDefaultRateProvider(BTCPayNetwork network)
|
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
|
||||||
{
|
{
|
||||||
if(network.DefaultRateProvider == null)
|
CacheSpan = CacheSpan
|
||||||
|
};
|
||||||
|
var value = await cached.GetRatesAsync();
|
||||||
|
return new QueryRateResult()
|
||||||
{
|
{
|
||||||
throw new RateUnavailableException(network.CryptoCode);
|
CachedResult = !fallback.Used,
|
||||||
}
|
ExchangeRates = value,
|
||||||
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
|
Exceptions = fallback.Exceptions
|
||||||
|
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,13 @@ using System.Collections.Generic;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
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 class BitpayRateProvider : IRateProvider
|
||||||
{
|
{
|
||||||
|
public const string BitpayName = "bitpay";
|
||||||
Bitpay _Bitpay;
|
Bitpay _Bitpay;
|
||||||
public BitpayRateProvider(Bitpay bitpay)
|
public BitpayRateProvider(Bitpay bitpay)
|
||||||
{
|
{
|
||||||
@@ -24,21 +19,13 @@ namespace BTCPayServer.Services.Rates
|
|||||||
throw new ArgumentNullException(nameof(bitpay));
|
throw new ArgumentNullException(nameof(bitpay));
|
||||||
_Bitpay = 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
|
.AllRates
|
||||||
.Select(r => new Rate() { Currency = r.Code, Value = r.Value })
|
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
|
||||||
.ToList();
|
.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
@@ -10,9 +11,8 @@ namespace BTCPayServer.Services.Rates
|
|||||||
{
|
{
|
||||||
private IRateProvider _Inner;
|
private IRateProvider _Inner;
|
||||||
private IMemoryCache _MemoryCache;
|
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)
|
if (inner == null)
|
||||||
throw new ArgumentNullException(nameof(inner));
|
throw new ArgumentNullException(nameof(inner));
|
||||||
@@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates
|
|||||||
throw new ArgumentNullException(nameof(memoryCache));
|
throw new ArgumentNullException(nameof(memoryCache));
|
||||||
this._Inner = inner;
|
this._Inner = inner;
|
||||||
this.MemoryCache = memoryCache;
|
this.MemoryCache = memoryCache;
|
||||||
this._CryptoCode = cryptoCode;
|
this.ExchangeName = exchangeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRateProvider Inner
|
public IRateProvider Inner
|
||||||
@@ -31,6 +31,8 @@ namespace BTCPayServer.Services.Rates
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ExchangeName { get; set; }
|
||||||
|
|
||||||
public TimeSpan CacheSpan
|
public TimeSpan CacheSpan
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -38,24 +40,13 @@ namespace BTCPayServer.Services.Rates
|
|||||||
} = TimeSpan.FromMinutes(1.0);
|
} = TimeSpan.FromMinutes(1.0);
|
||||||
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
|
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
|
||||||
|
|
||||||
public Task<decimal> GetRateAsync(string currency)
|
public Task<ExchangeRates> GetRatesAsync()
|
||||||
{
|
{
|
||||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||||
{
|
|
||||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
|
||||||
return _Inner.GetRateAsync(currency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<ICollection<Rate>> GetRatesAsync()
|
|
||||||
{
|
|
||||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
|
||||||
{
|
{
|
||||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||||
return _Inner.GetRatesAsync();
|
return _Inner.GetRatesAsync();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public string AdditionalScope { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
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 GetExchangeTickersResponse
|
||||||
{
|
{
|
||||||
public class Exchange
|
public class Exchange
|
||||||
@@ -73,14 +51,21 @@ namespace BTCPayServer.Services.Rates
|
|||||||
|
|
||||||
public class CoinAverageRateProvider : IRateProvider
|
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();
|
static HttpClient _Client = new HttpClient();
|
||||||
|
|
||||||
public CoinAverageRateProvider(string cryptoCode)
|
public string Exchange { get; set; } = CoinAverageName;
|
||||||
{
|
|
||||||
CryptoCode = cryptoCode ?? "BTC";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Exchange { get; set; }
|
|
||||||
|
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
|
|
||||||
@@ -88,26 +73,18 @@ namespace BTCPayServer.Services.Rates
|
|||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = "global";
|
} = "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; }
|
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"
|
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}";
|
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
@@ -128,34 +105,32 @@ namespace BTCPayServer.Services.Rates
|
|||||||
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
||||||
resp.EnsureSuccessStatusCode();
|
resp.EnsureSuccessStatusCode();
|
||||||
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||||
if(Exchange != null)
|
if (Exchange != CoinAverageName)
|
||||||
{
|
{
|
||||||
rates = (JObject)rates["symbols"];
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryToDecimal(JProperty p, out decimal v)
|
var exchangeRates = new ExchangeRates();
|
||||||
|
foreach (var prop in rates.Properties())
|
||||||
{
|
{
|
||||||
JToken token = p.Value[Exchange == null ? "last" : "bid"];
|
ExchangeRate exchangeRate = new ExchangeRate();
|
||||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
public async Task TestAuthAsync()
|
||||||
|
|||||||
@@ -20,12 +20,35 @@ namespace BTCPayServer.Services.Rates
|
|||||||
return _Settings.AddHeader(message);
|
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
|
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||||
{
|
{
|
||||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
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 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()
|
public CoinAverageSettings()
|
||||||
{
|
{
|
||||||
@@ -37,8 +60,9 @@ namespace BTCPayServer.Services.Rates
|
|||||||
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
|
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
|
||||||
//}
|
//}
|
||||||
//b.AppendLine("}.ToArray()");
|
//b.AppendLine("}.ToArray()");
|
||||||
|
AvailableExchanges = new CoinAverageExchanges();
|
||||||
AvailableExchanges = new[] {
|
foreach(var item in
|
||||||
|
new[] {
|
||||||
(DisplayName: "BitBargain", Name: "bitbargain"),
|
(DisplayName: "BitBargain", Name: "bitbargain"),
|
||||||
(DisplayName: "Tidex", Name: "tidex"),
|
(DisplayName: "Tidex", Name: "tidex"),
|
||||||
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
||||||
@@ -89,7 +113,10 @@ namespace BTCPayServer.Services.Rates
|
|||||||
(DisplayName: "Quoine", Name: "quoine"),
|
(DisplayName: "Quoine", Name: "quoine"),
|
||||||
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
||||||
(DisplayName: "Bitso", Name: "bitso"),
|
(DisplayName: "Bitso", Name: "bitso"),
|
||||||
}.ToArray();
|
})
|
||||||
|
{
|
||||||
|
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AddHeader(HttpRequestMessage message)
|
public Task AddHeader(HttpRequestMessage message)
|
||||||
|
|||||||
@@ -2,58 +2,35 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
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
|
public class FallbackRateProvider : IRateProvider
|
||||||
{
|
{
|
||||||
|
|
||||||
IRateProvider[] _Providers;
|
IRateProvider[] _Providers;
|
||||||
|
public bool Used { get; set; }
|
||||||
public FallbackRateProvider(IRateProvider[] providers)
|
public FallbackRateProvider(IRateProvider[] providers)
|
||||||
{
|
{
|
||||||
if (providers == null)
|
if (providers == null)
|
||||||
throw new ArgumentNullException(nameof(providers));
|
throw new ArgumentNullException(nameof(providers));
|
||||||
_Providers = 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)
|
foreach (var p in _Providers)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await p.GetRatesAsync().ConfigureAwait(false);
|
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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
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
|
public interface IRateProvider
|
||||||
{
|
{
|
||||||
Task<decimal> GetRateAsync(string currency);
|
Task<ExchangeRates> GetRatesAsync();
|
||||||
Task<ICollection<Rate>> GetRatesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,32 +4,15 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
namespace BTCPayServer.Services.Rates
|
||||||
{
|
{
|
||||||
public class QuadrigacxRateProvider : IRateProvider
|
public class QuadrigacxRateProvider : IRateProvider
|
||||||
{
|
{
|
||||||
public QuadrigacxRateProvider(string crypto)
|
public const string QuadrigacxName = "quadrigacx";
|
||||||
{
|
|
||||||
CryptoCode = crypto;
|
|
||||||
}
|
|
||||||
public string CryptoCode { get; set; }
|
|
||||||
static HttpClient _Client = new HttpClient();
|
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)
|
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);
|
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");
|
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||||
|
|
||||||
List<Rate> result = new List<Rate>();
|
var exchangeRates = new ExchangeRates();
|
||||||
foreach (var prop in rates.Properties())
|
foreach (var prop in rates.Properties())
|
||||||
{
|
{
|
||||||
var rate = new Rate();
|
var rate = new ExchangeRate();
|
||||||
var splitted = prop.Name.Split('_');
|
if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair))
|
||||||
var crypto = splitted[0].ToUpperInvariant();
|
continue;
|
||||||
if (crypto != CryptoCode)
|
rate.CurrencyPair = pair;
|
||||||
|
rate.Exchange = QuadrigacxName;
|
||||||
|
if (!TryToDecimal((JObject)prop.Value, out var v))
|
||||||
continue;
|
continue;
|
||||||
rate.Currency = splitted[1].ToUpperInvariant();
|
|
||||||
TryToDecimal((JObject)prop.Value, out var v);
|
|
||||||
rate.Value = v;
|
rate.Value = v;
|
||||||
result.Add(rate);
|
exchangeRates.Add(rate);
|
||||||
}
|
}
|
||||||
return result;
|
return exchangeRates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user