mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 14:04:12 +01:00
Merge remote-tracking branch 'source/master' into dev-lndrpc
This commit is contained in:
commit
6cefd9c3e7
152 changed files with 5599 additions and 1639 deletions
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
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.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
using BTCPayServer.Tests.Mocks;
|
using BTCPayServer.Tests.Mocks;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
@ -104,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)
|
||||||
|
@ -126,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
|
||||||
|
@ -142,7 +160,7 @@ namespace BTCPayServer.Tests
|
||||||
return _Host.Services.GetRequiredService<T>();
|
return _Host.Services.GetRequiredService<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetController<T>(string userId = null) where T : Controller
|
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||||
{
|
{
|
||||||
var context = new DefaultHttpContext();
|
var context = new DefaultHttpContext();
|
||||||
context.Request.Host = new HostString("127.0.0.1");
|
context.Request.Host = new HostString("127.0.0.1");
|
||||||
|
@ -150,7 +168,11 @@ namespace BTCPayServer.Tests
|
||||||
context.Request.Protocol = "http";
|
context.Request.Protocol = "http";
|
||||||
if (userId != null)
|
if (userId != null)
|
||||||
{
|
{
|
||||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
|
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||||
|
}
|
||||||
|
if(storeId != null)
|
||||||
|
{
|
||||||
|
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||||
}
|
}
|
||||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||||
var provider = scope.CreateScope().ServiceProvider;
|
var provider = scope.CreateScope().ServiceProvider;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
|
FROM microsoft/dotnet:2.1.300-rc1-sdk-alpine3.7
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# caches restore result by copying csproj file separately
|
# caches restore result by copying csproj file separately
|
||||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||||
|
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
142
BTCPayServer.Tests/RateRulesTest.cs
Normal file
142
BTCPayServer.Tests/RateRulesTest.cs
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Tests
|
||||||
|
{
|
||||||
|
public class RateRulesTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanParseRateRules()
|
||||||
|
{
|
||||||
|
// Check happy path
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// Some cool comments");
|
||||||
|
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||||
|
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||||
|
builder.AppendLine("// Some other cool comments");
|
||||||
|
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||||
|
builder.AppendLine("BTC_X = Coinbase(BTC_X);");
|
||||||
|
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||||
|
|
||||||
|
Assert.False(RateRules.TryParse("DPW*&W&#hdi&#&3JJD", out var rules));
|
||||||
|
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||||
|
Assert.Equal(
|
||||||
|
"// Some cool comments\n" +
|
||||||
|
"DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" +
|
||||||
|
"DOGE_BTC = bittrex(DOGE_BTC);\n" +
|
||||||
|
"// Some other cool comments\n" +
|
||||||
|
"BTC_USD = gdax(BTC_USD);\n" +
|
||||||
|
"BTC_X = coinbase(BTC_X);\n" +
|
||||||
|
"X_X = coinaverage(X_X) * 1.02;",
|
||||||
|
rules.ToString());
|
||||||
|
var tests = new[]
|
||||||
|
{
|
||||||
|
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||||
|
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
|
||||||
|
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||||
|
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
|
||||||
|
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
|
||||||
|
};
|
||||||
|
foreach (var test in tests)
|
||||||
|
{
|
||||||
|
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||||
|
}
|
||||||
|
rules.GlobalMultiplier = 2.32m;
|
||||||
|
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
|
||||||
|
////////////////
|
||||||
|
|
||||||
|
// Check errors conditions
|
||||||
|
builder = new StringBuilder();
|
||||||
|
builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1");
|
||||||
|
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||||
|
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||||
|
builder.AppendLine("LTC_CHF = LTC_CHF * 1.01");
|
||||||
|
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||||
|
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||||
|
|
||||||
|
tests = new[]
|
||||||
|
{
|
||||||
|
(Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"),
|
||||||
|
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"),
|
||||||
|
(Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"),
|
||||||
|
};
|
||||||
|
foreach (var test in tests)
|
||||||
|
{
|
||||||
|
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||||
|
}
|
||||||
|
//////////////////
|
||||||
|
|
||||||
|
// Check if we can resolve exchange rates
|
||||||
|
builder = new StringBuilder();
|
||||||
|
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||||
|
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||||
|
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||||
|
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||||
|
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||||
|
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||||
|
|
||||||
|
var tests2 = new[]
|
||||||
|
{
|
||||||
|
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||||
|
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
|
||||||
|
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||||
|
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
|
||||||
|
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
|
||||||
|
};
|
||||||
|
foreach (var test in tests2)
|
||||||
|
{
|
||||||
|
var rule = rules.GetRuleFor(CurrencyPair.Parse(test.Pair));
|
||||||
|
Assert.Equal(test.Expected, rule.ToString());
|
||||||
|
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
|
||||||
|
}
|
||||||
|
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
|
||||||
|
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000);
|
||||||
|
rule2.Reevaluate();
|
||||||
|
Assert.True(rule2.HasError);
|
||||||
|
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
|
||||||
|
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
|
||||||
|
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m);
|
||||||
|
rule2.Reevaluate();
|
||||||
|
Assert.False(rule2.HasError);
|
||||||
|
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||||
|
Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m);
|
||||||
|
////////
|
||||||
|
|
||||||
|
// Make sure parenthesis are correctly calculated
|
||||||
|
builder = new StringBuilder();
|
||||||
|
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X");
|
||||||
|
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||||
|
builder.AppendLine("DOGE_BTC = 2000");
|
||||||
|
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||||
|
rules.GlobalMultiplier = 1.1m;
|
||||||
|
|
||||||
|
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||||
|
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
|
||||||
|
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
|
||||||
|
Assert.True(rule2.Reevaluate());
|
||||||
|
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
|
||||||
|
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
|
||||||
|
|
||||||
|
// Test inverse
|
||||||
|
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
|
||||||
|
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
|
||||||
|
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
|
||||||
|
Assert.True(rule2.Reevaluate());
|
||||||
|
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
|
||||||
|
Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
|
||||||
|
////////
|
||||||
|
|
||||||
|
// Make sure kraken is not converted to CurrencyPair
|
||||||
|
builder = new StringBuilder();
|
||||||
|
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||||
|
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||||
|
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||||
|
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), 1000m);
|
||||||
|
Assert.True(rule2.Reevaluate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,29 +44,27 @@ namespace BTCPayServer.Tests
|
||||||
public async Task GrantAccessAsync()
|
public async Task GrantAccessAsync()
|
||||||
{
|
{
|
||||||
await RegisterAsync();
|
await RegisterAsync();
|
||||||
var store = await CreateStoreAsync();
|
await CreateStoreAsync();
|
||||||
|
var store = this.GetController<StoresController>();
|
||||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||||
await store.Pair(pairingCode.ToString(), StoreId);
|
await store.Pair(pairingCode.ToString(), StoreId);
|
||||||
}
|
}
|
||||||
public StoresController CreateStore()
|
public void CreateStore()
|
||||||
{
|
{
|
||||||
return CreateStoreAsync().GetAwaiter().GetResult();
|
CreateStoreAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetController<T>() where T : Controller
|
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||||
{
|
{
|
||||||
return parent.PayTester.GetController<T>(UserId);
|
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StoresController> CreateStoreAsync()
|
public async Task CreateStoreAsync()
|
||||||
{
|
{
|
||||||
var store = parent.PayTester.GetController<UserStoresController>(UserId);
|
var store = this.GetController<UserStoresController>();
|
||||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||||
StoreId = store.CreatedStoreId;
|
StoreId = store.CreatedStoreId;
|
||||||
var store2 = parent.PayTester.GetController<StoresController>(UserId);
|
|
||||||
store2.CreatedStoreId = store.CreatedStoreId;
|
|
||||||
return store2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||||
|
@ -78,12 +76,12 @@ namespace BTCPayServer.Tests
|
||||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||||
{
|
{
|
||||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||||
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
|
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
|
||||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||||
await store.UpdateStore(StoreId, vm);
|
await store.UpdateStore(vm);
|
||||||
|
|
||||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||||
{
|
{
|
||||||
|
@ -127,7 +125,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||||
{
|
{
|
||||||
var storeController = parent.PayTester.GetController<StoresController>(UserId);
|
var storeController = this.GetController<StoresController>();
|
||||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||||
{
|
{
|
||||||
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
||||||
|
|
|
@ -32,6 +32,12 @@ using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
|
using ExchangeSharp;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
|
@ -43,6 +49,27 @@ namespace BTCPayServer.Tests
|
||||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanHandleUriValidation()
|
||||||
|
{
|
||||||
|
var attribute = new UriAttribute();
|
||||||
|
Assert.True(attribute.IsValid("http://localhost"));
|
||||||
|
Assert.True(attribute.IsValid("http://localhost:1234"));
|
||||||
|
Assert.True(attribute.IsValid("https://localhost"));
|
||||||
|
Assert.True(attribute.IsValid("https://127.0.0.1"));
|
||||||
|
Assert.True(attribute.IsValid("http://127.0.0.1"));
|
||||||
|
Assert.True(attribute.IsValid("http://127.0.0.1:1234"));
|
||||||
|
Assert.True(attribute.IsValid("http://gozo.com"));
|
||||||
|
Assert.True(attribute.IsValid("https://gozo.com"));
|
||||||
|
Assert.True(attribute.IsValid("https://gozo.com:1234"));
|
||||||
|
Assert.True(attribute.IsValid("https://gozo.com:1234/test.css"));
|
||||||
|
Assert.True(attribute.IsValid("https://gozo.com:1234/test.png"));
|
||||||
|
Assert.False(attribute.IsValid("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud e"));
|
||||||
|
Assert.False(attribute.IsValid(2));
|
||||||
|
Assert.False(attribute.IsValid("http://"));
|
||||||
|
Assert.False(attribute.IsValid("httpdsadsa.com"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanCalculateCryptoDue2()
|
public void CanCalculateCryptoDue2()
|
||||||
{
|
{
|
||||||
|
@ -105,22 +132,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);
|
||||||
|
@ -235,6 +251,89 @@ namespace BTCPayServer.Tests
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanAcceptInvoiceWithTolerance()
|
||||||
|
{
|
||||||
|
var entity = new InvoiceEntity();
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
entity.Payments = new List<PaymentEntity>();
|
||||||
|
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
|
||||||
|
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||||
|
entity.PaymentTolerance = 0;
|
||||||
|
|
||||||
|
|
||||||
|
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
|
||||||
|
var accounting = paymentMethod.Calculate();
|
||||||
|
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||||
|
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||||
|
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
|
||||||
|
|
||||||
|
entity.PaymentTolerance = 10;
|
||||||
|
accounting = paymentMethod.Calculate();
|
||||||
|
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
|
||||||
|
|
||||||
|
entity.PaymentTolerance = 100;
|
||||||
|
accounting = paymentMethod.Calculate();
|
||||||
|
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanAcceptInvoiceWithTolerance2()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
// Set tolerance to 50%
|
||||||
|
var stores = user.GetController<StoresController>();
|
||||||
|
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(stores.UpdateStore()).Model);
|
||||||
|
Assert.Equal(0.0, vm.PaymentTolerance);
|
||||||
|
vm.PaymentTolerance = 50.0;
|
||||||
|
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result);
|
||||||
|
|
||||||
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
|
{
|
||||||
|
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||||
|
Price = 5000.0m,
|
||||||
|
Currency = "USD",
|
||||||
|
PosData = "posData",
|
||||||
|
OrderId = "orderId",
|
||||||
|
ItemDesc = "Some description",
|
||||||
|
FullNotifications = true
|
||||||
|
}, Facade.Merchant);
|
||||||
|
|
||||||
|
// Pays 75%
|
||||||
|
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||||
|
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m));
|
||||||
|
|
||||||
|
Eventually(() =>
|
||||||
|
{
|
||||||
|
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||||
|
Assert.Equal("paid", localInvoice.Status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundupCurrenciesCorrectly()
|
||||||
|
{
|
||||||
|
foreach(var test in new[]
|
||||||
|
{
|
||||||
|
(0.0005m, "$0.0005 (USD)"),
|
||||||
|
(0.001m, "$0.001 (USD)"),
|
||||||
|
(0.01m, "$0.01 (USD)"),
|
||||||
|
(0.1m, "$0.10 (USD)"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
|
||||||
|
Assert.Equal(test.Item2, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanPayUsingBIP70()
|
public void CanPayUsingBIP70()
|
||||||
{
|
{
|
||||||
|
@ -247,7 +346,7 @@ namespace BTCPayServer.Tests
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -303,9 +402,9 @@ namespace BTCPayServer.Tests
|
||||||
tester.Start();
|
tester.Start();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
user.GrantAccess();
|
||||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
var storeController = user.GetController<StoresController>();
|
||||||
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
|
Assert.IsType<ViewResult>(storeController.UpdateStore());
|
||||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult());
|
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
|
||||||
|
|
||||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||||
{
|
{
|
||||||
|
@ -319,7 +418,7 @@ namespace BTCPayServer.Tests
|
||||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||||
}, "save", "BTC").GetAwaiter().GetResult());
|
}, "save", "BTC").GetAwaiter().GetResult());
|
||||||
|
|
||||||
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
|
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore()).Model);
|
||||||
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
|
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -393,7 +492,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 0.01,
|
Price = 0.01m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -426,7 +525,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 0.01,
|
Price = 0.01m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -454,7 +553,7 @@ namespace BTCPayServer.Tests
|
||||||
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
|
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
|
||||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 0.01,
|
Price = 0.01m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -479,8 +578,8 @@ namespace BTCPayServer.Tests
|
||||||
acc.Register();
|
acc.Register();
|
||||||
acc.CreateStore();
|
acc.CreateStore();
|
||||||
|
|
||||||
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
|
var controller = acc.GetController<StoresController>();
|
||||||
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
|
var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel()
|
||||||
{
|
{
|
||||||
Facade = Facade.Merchant.ToString(),
|
Facade = Facade.Merchant.ToString(),
|
||||||
Label = "bla",
|
Label = "bla",
|
||||||
|
@ -507,7 +606,7 @@ namespace BTCPayServer.Tests
|
||||||
acc.RegisterDerivationScheme("BTC");
|
acc.RegisterDerivationScheme("BTC");
|
||||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5.0,
|
Price = 5.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -538,17 +637,66 @@ namespace BTCPayServer.Tests
|
||||||
tester.Start();
|
tester.Start();
|
||||||
var acc = tester.NewAccount();
|
var acc = tester.NewAccount();
|
||||||
acc.Register();
|
acc.Register();
|
||||||
var store = acc.CreateStore();
|
acc.CreateStore();
|
||||||
|
var store = acc.GetController<StoresController>();
|
||||||
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||||
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
|
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
|
||||||
|
|
||||||
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
||||||
var store2 = acc.CreateStore();
|
acc.CreateStore();
|
||||||
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
|
var store2 = acc.GetController<StoresController>();
|
||||||
|
store2.Pair(pairingCode.ToString(), store2.StoreData.Id).GetAwaiter().GetResult();
|
||||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
|
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanListInvoices()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var acc = tester.NewAccount();
|
||||||
|
acc.GrantAccess();
|
||||||
|
acc.RegisterDerivationScheme("BTC");
|
||||||
|
// First we try payment with a merchant having only BTC
|
||||||
|
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||||
|
{
|
||||||
|
Price = 500,
|
||||||
|
Currency = "USD",
|
||||||
|
PosData = "posData",
|
||||||
|
OrderId = "orderId",
|
||||||
|
ItemDesc = "Some description",
|
||||||
|
FullNotifications = true
|
||||||
|
}, Facade.Merchant);
|
||||||
|
|
||||||
|
var cashCow = tester.ExplorerNode;
|
||||||
|
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||||
|
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
|
||||||
|
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||||
|
Eventually(() =>
|
||||||
|
{
|
||||||
|
invoice = acc.BitPay.GetInvoice(invoice.Id);
|
||||||
|
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}");
|
||||||
|
AssertSearchInvoice(acc, false, invoice.Id, $"storeid:blah");
|
||||||
|
AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}");
|
||||||
|
AssertSearchInvoice(acc, true, invoice.Id, $"exceptionstatus:paidPartial");
|
||||||
|
AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver");
|
||||||
|
AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true");
|
||||||
|
AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
|
||||||
|
{
|
||||||
|
var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>().ListInvoices(filter).Result).Model;
|
||||||
|
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanRBFPayment()
|
public void CanRBFPayment()
|
||||||
{
|
{
|
||||||
|
@ -560,7 +708,7 @@ namespace BTCPayServer.Tests
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD"
|
Currency = "USD"
|
||||||
}, Facade.Merchant);
|
}, Facade.Merchant);
|
||||||
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
|
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
|
||||||
|
@ -637,6 +785,36 @@ namespace BTCPayServer.Tests
|
||||||
user.GrantAccess();
|
user.GrantAccess();
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||||
|
|
||||||
|
// Can generate API Key
|
||||||
|
var repo = tester.PayTester.GetService<TokenRepository>();
|
||||||
|
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||||
|
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
|
||||||
|
|
||||||
|
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||||
|
///////
|
||||||
|
|
||||||
|
// Generating a new one remove the previous
|
||||||
|
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
|
||||||
|
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||||
|
Assert.NotEqual(apiKey, apiKey2);
|
||||||
|
////////
|
||||||
|
|
||||||
|
apiKey = apiKey2;
|
||||||
|
|
||||||
|
// Can create an invoice with this new API Key
|
||||||
|
HttpClient client = new HttpClient();
|
||||||
|
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, tester.PayTester.ServerUri.AbsoluteUri + "invoices");
|
||||||
|
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey)));
|
||||||
|
var invoice = new Invoice()
|
||||||
|
{
|
||||||
|
Price = 5000.0m,
|
||||||
|
Currency = "USD"
|
||||||
|
};
|
||||||
|
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
|
||||||
|
var result = client.SendAsync(message).GetAwaiter().GetResult();
|
||||||
|
result.EnsureSuccessStatusCode();
|
||||||
|
/////////////////////
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -666,13 +844,13 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
||||||
{
|
{
|
||||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
var storeController = user.GetController<StoresController>();
|
||||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
|
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||||
vm.PreferredExchange = exchange;
|
vm.PreferredExchange = exchange;
|
||||||
storeController.UpdateStore(user.StoreId, vm).Wait();
|
storeController.Rates(vm).Wait();
|
||||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -696,7 +874,7 @@ namespace BTCPayServer.Tests
|
||||||
// First we try payment with a merchant having only BTC
|
// First we try payment with a merchant having only BTC
|
||||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -705,16 +883,16 @@ namespace BTCPayServer.Tests
|
||||||
}, Facade.Merchant);
|
}, Facade.Merchant);
|
||||||
|
|
||||||
|
|
||||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
var storeController = user.GetController<StoresController>();
|
||||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
|
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||||
Assert.Equal(1.0, vm.RateMultiplier);
|
Assert.Equal(1.0, vm.RateMultiplier);
|
||||||
vm.RateMultiplier = 0.5;
|
vm.RateMultiplier = 0.5;
|
||||||
storeController.UpdateStore(user.StoreId, vm).Wait();
|
storeController.Rates(vm).Wait();
|
||||||
|
|
||||||
|
|
||||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -749,6 +927,11 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
Assert.Single(invoice.CryptoInfo);
|
Assert.Single(invoice.CryptoInfo);
|
||||||
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
||||||
|
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
|
||||||
|
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
|
||||||
|
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||||
|
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||||
|
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||||
var cashCow = tester.LTCExplorerNode;
|
var cashCow = tester.LTCExplorerNode;
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||||
var firstPayment = Money.Coins(0.1m);
|
var firstPayment = Money.Coins(0.1m);
|
||||||
|
@ -770,7 +953,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
|
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
|
||||||
Assert.Null(invoice.BitcoinAddress);
|
Assert.Null(invoice.BitcoinAddress);
|
||||||
Assert.NotEqual(1.0, invoice.Rate);
|
Assert.NotEqual(1.0m, invoice.Rate);
|
||||||
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
|
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
|
||||||
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
|
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
|
||||||
|
|
||||||
|
@ -785,6 +968,66 @@ namespace BTCPayServer.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanModifyRates()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
var store = user.GetController<StoresController>();
|
||||||
|
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.False(rateVm.ShowScripting);
|
||||||
|
Assert.Equal("coinaverage", rateVm.PreferredExchange);
|
||||||
|
Assert.Equal(1.0, rateVm.RateMultiplier);
|
||||||
|
Assert.Null(rateVm.TestRateRules);
|
||||||
|
|
||||||
|
rateVm.PreferredExchange = "bitflyer";
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.Equal("bitflyer", rateVm.PreferredExchange);
|
||||||
|
|
||||||
|
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
|
||||||
|
rateVm.RateMultiplier = 1.1;
|
||||||
|
store = user.GetController<StoresController>();
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||||
|
Assert.NotNull(rateVm.TestRateRules);
|
||||||
|
Assert.Equal(2, rateVm.TestRateRules.Count);
|
||||||
|
Assert.False(rateVm.TestRateRules[0].Error);
|
||||||
|
Assert.StartsWith("(bitflyer(BTC_JPY)) * 1.10 =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.True(rateVm.TestRateRules[1].Error);
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
store = user.GetController<StoresController>();
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
|
||||||
|
Assert.True(rateVm.ShowScripting);
|
||||||
|
rateVm.ScriptTest = "BTC_JPY";
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||||
|
Assert.True(rateVm.ShowScripting);
|
||||||
|
Assert.Contains("(bitflyer(BTC_JPY)) * 1.10 = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
|
||||||
|
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
|
||||||
|
"X_CAD = quadrigacx(X_CAD);\n" +
|
||||||
|
"X_X = gdax(X_X);";
|
||||||
|
rateVm.RateMultiplier = 0.5;
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||||
|
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
|
||||||
|
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||||
|
store = user.GetController<StoresController>();
|
||||||
|
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||||
|
Assert.Equal(0.5, rateVm.RateMultiplier);
|
||||||
|
Assert.True(rateVm.ShowScripting);
|
||||||
|
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanPayWithTwoCurrencies()
|
public void CanPayWithTwoCurrencies()
|
||||||
{
|
{
|
||||||
|
@ -797,7 +1040,7 @@ namespace BTCPayServer.Tests
|
||||||
// First we try payment with a merchant having only BTC
|
// First we try payment with a merchant having only BTC
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -823,13 +1066,23 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Single(checkout.AvailableCryptos);
|
Assert.Single(checkout.AvailableCryptos);
|
||||||
Assert.Equal("BTC", checkout.CryptoCode);
|
Assert.Equal("BTC", checkout.CryptoCode);
|
||||||
|
|
||||||
|
Assert.Single(invoice.PaymentCodes);
|
||||||
|
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||||
|
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||||
|
Assert.Single(invoice.PaymentSubtotals);
|
||||||
|
Assert.Single(invoice.PaymentTotals);
|
||||||
|
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
|
||||||
|
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
|
||||||
|
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
|
||||||
|
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
|
||||||
|
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
|
||||||
//////////////////////
|
//////////////////////
|
||||||
|
|
||||||
// Retry now with LTC enabled
|
// Retry now with LTC enabled
|
||||||
user.RegisterDerivationScheme("LTC");
|
user.RegisterDerivationScheme("LTC");
|
||||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -871,6 +1124,18 @@ namespace BTCPayServer.Tests
|
||||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
|
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
|
||||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||||
Assert.Equal("LTC", checkout.CryptoCode);
|
Assert.Equal("LTC", checkout.CryptoCode);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal(2, invoice.PaymentCodes.Count());
|
||||||
|
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||||
|
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||||
|
Assert.Equal(2, invoice.PaymentSubtotals.Count());
|
||||||
|
Assert.Equal(2, invoice.PaymentTotals.Count());
|
||||||
|
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
|
||||||
|
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
|
||||||
|
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||||
|
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||||
|
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -944,14 +1209,14 @@ namespace BTCPayServer.Tests
|
||||||
user.GrantAccess();
|
user.GrantAccess();
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||||
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId).Result).Model);
|
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
|
||||||
vm.LightningMaxValue = "2 USD";
|
vm.LightningMaxValue = "2 USD";
|
||||||
vm.OnChainMinValue = "5 USD";
|
vm.OnChainMinValue = "5 USD";
|
||||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId, vm).Result);
|
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm).Result);
|
||||||
|
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 1.5,
|
Price = 1.5m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -964,7 +1229,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5.5,
|
Price = 5.5m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -1013,7 +1278,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||||
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
|
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
|
||||||
var invoice = user.BitPay.GetInvoices().First();
|
var invoice = user.BitPay.GetInvoices().First();
|
||||||
Assert.Equal(10.00, invoice.Price);
|
Assert.Equal(10.00m, invoice.Price);
|
||||||
Assert.Equal("CAD", invoice.Currency);
|
Assert.Equal("CAD", invoice.Currency);
|
||||||
Assert.Equal("orange", invoice.ItemDesc);
|
Assert.Equal("orange", invoice.ItemDesc);
|
||||||
}
|
}
|
||||||
|
@ -1064,7 +1329,7 @@ namespace BTCPayServer.Tests
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -1107,8 +1372,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);
|
||||||
|
@ -1171,12 +1434,12 @@ namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||||
Assert.Equal("complete", localInvoice.Status);
|
Assert.Equal("complete", localInvoice.Status);
|
||||||
Assert.NotEqual(0.0, localInvoice.Rate);
|
Assert.NotEqual(0.0m, localInvoice.Rate);
|
||||||
});
|
});
|
||||||
|
|
||||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 5000.0,
|
Price = 5000.0m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
|
@ -1212,40 +1475,102 @@ 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");
|
[Fact]
|
||||||
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
public void CanQueryDirectProviders()
|
||||||
Assert.NotEmpty(rates);
|
{
|
||||||
Assert.NotEqual(0.0m, rates.First().Value);
|
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
var factory = CreateBTCPayRateFactory(provider);
|
||||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
|
||||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
foreach (var result in factory
|
||||||
|
.DirectProviders
|
||||||
|
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
var exchangeRates = result.ResultAsync.Result;
|
||||||
|
Assert.NotNull(exchangeRates);
|
||||||
|
Assert.NotEmpty(exchangeRates);
|
||||||
|
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||||
|
|
||||||
|
// This check if the currency pair is using right currency pair
|
||||||
|
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||||
|
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||||
|
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
|
||||||
|
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
|
||||||
|
&& e.Value > 1.0m // 1BTC will always be more than 1USD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanGetRateCryptoCurrenciesByDefault()
|
||||||
|
{
|
||||||
|
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||||
|
var factory = CreateBTCPayRateFactory(provider);
|
||||||
|
|
||||||
|
var pairs =
|
||||||
|
provider.GetAll()
|
||||||
|
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||||
|
var result = factory.FetchRates(pairs, rules);
|
||||||
|
foreach (var value in result)
|
||||||
|
{
|
||||||
|
var rateResult = value.Value.GetAwaiter().GetResult();
|
||||||
|
Assert.NotNull(rateResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider)
|
||||||
|
{
|
||||||
|
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
[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();
|
||||||
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 = CreateBTCPayRateFactory(provider);
|
||||||
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
factory.CacheSpan = TimeSpan.FromSeconds(10);
|
||||||
//Manually check that cache get hit after 10 sec
|
|
||||||
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)
|
||||||
|
|
|
@ -1,16 +1,5 @@
|
||||||
using BTCPayServer.Payments.Lightning.Lnd;
|
using System;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
|
||||||
using NBitpayClient;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Security;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
|
|
|
@ -62,7 +62,7 @@ services:
|
||||||
|
|
||||||
|
|
||||||
nbxplorer:
|
nbxplorer:
|
||||||
image: nicolasdorier/nbxplorer:1.0.2.2
|
image: nicolasdorier/nbxplorer:1.0.2.6
|
||||||
ports:
|
ports:
|
||||||
- "32838:32838"
|
- "32838:32838"
|
||||||
expose:
|
expose:
|
||||||
|
@ -110,14 +110,15 @@ services:
|
||||||
- "bitcoin_datadir:/data"
|
- "bitcoin_datadir:/data"
|
||||||
|
|
||||||
customer_lightningd:
|
customer_lightningd:
|
||||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
image: nicolasdorier/clightning:0.0.0.16-dev
|
||||||
environment:
|
environment:
|
||||||
EXPOSE_TCP: "true"
|
EXPOSE_TCP: "true"
|
||||||
LIGHTNINGD_OPT: |
|
LIGHTNINGD_OPT: |
|
||||||
bitcoin-datadir=/etc/bitcoin
|
bitcoin-datadir=/etc/bitcoin
|
||||||
bitcoin-rpcconnect=bitcoind
|
bitcoin-rpcconnect=bitcoind
|
||||||
network=regtest
|
network=regtest
|
||||||
ipaddr=customer_lightningd
|
bind-addr=0.0.0.0
|
||||||
|
announce-addr=customer_lightningd
|
||||||
log-level=debug
|
log-level=debug
|
||||||
dev-broadcast-interval=1000
|
dev-broadcast-interval=1000
|
||||||
ports:
|
ports:
|
||||||
|
@ -151,13 +152,14 @@ services:
|
||||||
- merchant_lightningd
|
- merchant_lightningd
|
||||||
|
|
||||||
merchant_lightningd:
|
merchant_lightningd:
|
||||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
image: nicolasdorier/clightning:0.0.0.14-dev
|
||||||
environment:
|
environment:
|
||||||
EXPOSE_TCP: "true"
|
EXPOSE_TCP: "true"
|
||||||
LIGHTNINGD_OPT: |
|
LIGHTNINGD_OPT: |
|
||||||
bitcoin-datadir=/etc/bitcoin
|
bitcoin-datadir=/etc/bitcoin
|
||||||
bitcoin-rpcconnect=bitcoind
|
bitcoin-rpcconnect=bitcoind
|
||||||
ipaddr=merchant_lightningd
|
bind-addr=0.0.0.0
|
||||||
|
announce-addr=merchant_lightningd
|
||||||
network=regtest
|
network=regtest
|
||||||
log-level=debug
|
log-level=debug
|
||||||
dev-broadcast-interval=1000
|
dev-broadcast-interval=1000
|
||||||
|
|
|
@ -45,6 +45,46 @@ namespace BTCPayServer.Authentication
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<String> GetStoreIdFromAPIKey(string apiKey)
|
||||||
|
{
|
||||||
|
using (var ctx = _Factory.CreateContext())
|
||||||
|
{
|
||||||
|
return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GenerateLegacyAPIKey(string storeId)
|
||||||
|
{
|
||||||
|
// It is legacy support and Bitpay generate string of unknown format, trying to replicate them
|
||||||
|
// as good as possible. The string below got generated for me.
|
||||||
|
var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE";
|
||||||
|
var rand = new Random(Math.Abs(RandomUtils.GetInt32()));
|
||||||
|
var generated = new char[chars.Length];
|
||||||
|
for (int i = 0; i < generated.Length; i++)
|
||||||
|
{
|
||||||
|
generated[i] = chars[rand.Next(0, generated.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var ctx = _Factory.CreateContext())
|
||||||
|
{
|
||||||
|
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
ctx.ApiKeys.Remove(existing);
|
||||||
|
}
|
||||||
|
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||||
|
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string[]> GetLegacyAPIKeys(string storeId)
|
||||||
|
{
|
||||||
|
using (var ctx = _Factory.CreateContext())
|
||||||
|
{
|
||||||
|
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
||||||
{
|
{
|
||||||
return new BitTokenEntity()
|
return new BitTokenEntity()
|
||||||
|
|
|
@ -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),
|
||||||
|
|
29
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
29
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public partial class BTCPayNetworkProvider
|
||||||
|
{
|
||||||
|
public void InitBitcoinGold()
|
||||||
|
{
|
||||||
|
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
|
||||||
|
Add(new BTCPayNetwork()
|
||||||
|
{
|
||||||
|
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||||
|
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
|
||||||
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
|
UriScheme = "bitcoingold",
|
||||||
|
DefaultRateRules = new[]
|
||||||
|
{
|
||||||
|
"BTG_X = BTG_BTC * BTC_X",
|
||||||
|
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||||
|
},
|
||||||
|
CryptoImagePath = "imlegacy/btg-symbol.svg",
|
||||||
|
LightningImagePath = "imlegacy/btg-symbol.svg",
|
||||||
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||||
|
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,11 @@ namespace BTCPayServer
|
||||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
UriScheme = "dogecoin",
|
UriScheme = "dogecoin",
|
||||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
DefaultRateRules = new[]
|
||||||
|
{
|
||||||
|
"DOGE_X = DOGE_BTC * BTC_X",
|
||||||
|
"DOGE_BTC = bittrex(DOGE_BTC)"
|
||||||
|
},
|
||||||
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),
|
||||||
|
|
35
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBXplorer;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public partial class BTCPayNetworkProvider
|
||||||
|
{
|
||||||
|
public void InitMonacoin()
|
||||||
|
{
|
||||||
|
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
|
||||||
|
Add(new BTCPayNetwork()
|
||||||
|
{
|
||||||
|
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||||
|
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
|
||||||
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
|
UriScheme = "monacoin",
|
||||||
|
DefaultRateRules = new[]
|
||||||
|
{
|
||||||
|
"MONA_X = MONA_BTC * BTC_X",
|
||||||
|
"MONA_BTC = zaif(MONA_BTC)"
|
||||||
|
},
|
||||||
|
CryptoImagePath = "imlegacy/monacoin.png",
|
||||||
|
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||||
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||||
|
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,8 @@ namespace BTCPayServer
|
||||||
InitBitcoin();
|
InitBitcoin();
|
||||||
InitLitecoin();
|
InitLitecoin();
|
||||||
InitDogecoin();
|
InitDogecoin();
|
||||||
|
InitBitcoinGold();
|
||||||
|
InitMonacoin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -86,7 +88,11 @@ namespace BTCPayServer
|
||||||
|
|
||||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||||
{
|
{
|
||||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
|
||||||
|
{
|
||||||
|
if (cryptoCode == "XBT")
|
||||||
|
return GetNetwork("BTC");
|
||||||
|
}
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<Version>1.0.1.93</Version>
|
<Version>1.0.2.21</Version>
|
||||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -30,38 +30,35 @@
|
||||||
<EmbeddedResource Include="Currencies.txt" />
|
<EmbeddedResource Include="Currencies.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
|
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.0" />
|
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||||
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
|
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-rc1-final" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||||
<PackageReference Include="NBitcoin" Version="4.1.1.3" />
|
<PackageReference Include="NBitcoin" Version="4.1.1.7" />
|
||||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
<PackageReference Include="NBitpayClient" Version="1.0.0.23" />
|
||||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
|
<PackageReference Include="NBXplorer.Client" Version="1.0.2.8" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1-rc1" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
|
||||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
|
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0-rc1-final" PrivateAssets="All" />
|
||||||
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
|
|
||||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
|
||||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
|
[BitpayAPIConstraint]
|
||||||
public class AccessTokenController : Controller
|
public class AccessTokenController : Controller
|
||||||
{
|
{
|
||||||
TokenRepository _TokenRepository;
|
TokenRepository _TokenRepository;
|
||||||
|
|
|
@ -16,10 +16,11 @@ using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[Route("[controller]/[action]")]
|
[Route("[controller]/[action]")]
|
||||||
public class AccountController : Controller
|
public class AccountController : Controller
|
||||||
{
|
{
|
||||||
|
|
|
@ -162,7 +162,8 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{appId}/pos")]
|
[Route("{appId}/pos")]
|
||||||
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
|
[IgnoreAntiforgeryToken]
|
||||||
|
public async Task<IActionResult> ViewPointOfSale(string appId, decimal amount, string choiceKey)
|
||||||
{
|
{
|
||||||
var app = await GetApp(appId, AppType.PointOfSale);
|
var app = await GetApp(appId, AppType.PointOfSale);
|
||||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||||
|
@ -177,7 +178,7 @@ namespace BTCPayServer.Controllers
|
||||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||||
}
|
}
|
||||||
string title = null;
|
string title = null;
|
||||||
double price = 0.0;
|
var price = 0.0m;
|
||||||
if (!string.IsNullOrEmpty(choiceKey))
|
if (!string.IsNullOrEmpty(choiceKey))
|
||||||
{
|
{
|
||||||
var choices = Parse(settings.Template, settings.Currency);
|
var choices = Parse(settings.Template, settings.Currency);
|
||||||
|
@ -185,7 +186,7 @@ namespace BTCPayServer.Controllers
|
||||||
if (choice == null)
|
if (choice == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
title = choice.Title;
|
title = choice.Title;
|
||||||
price = (double)choice.Price.Value;
|
price = choice.Price.Value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers
|
||||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
if (app == null)
|
||||||
|
return null;
|
||||||
if (type != null && type.Value.ToString() != app.AppType)
|
if (type != null && type.Value.ToString() != app.AppType)
|
||||||
return null;
|
return null;
|
||||||
return app;
|
return app;
|
||||||
|
@ -174,24 +176,19 @@ namespace BTCPayServer.Controllers
|
||||||
using (var ctx = _ContextFactory.CreateContext())
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
return await ctx.UserStore
|
return await ctx.UserStore
|
||||||
.Where(us => us.ApplicationUserId == userId)
|
.Where(us => us.ApplicationUserId == userId)
|
||||||
.Select(us => new
|
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||||
{
|
(us, app) =>
|
||||||
IsOwner = us.Role == StoreRoles.Owner,
|
new ListAppsViewModel.ListAppViewModel()
|
||||||
StoreId = us.StoreDataId,
|
{
|
||||||
StoreName = us.StoreData.StoreName,
|
IsOwner = us.Role == StoreRoles.Owner,
|
||||||
Apps = us.StoreData.Apps
|
StoreId = us.StoreDataId,
|
||||||
})
|
StoreName = us.StoreData.StoreName,
|
||||||
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
|
AppName = app.Name,
|
||||||
{
|
AppType = app.AppType,
|
||||||
IsOwner = us.IsOwner,
|
Id = app.Id
|
||||||
AppName = app.Name,
|
})
|
||||||
AppType = app.AppType,
|
.ToArrayAsync();
|
||||||
Id = app.Id,
|
|
||||||
StoreId = us.StoreId,
|
|
||||||
StoreName = us.StoreName
|
|
||||||
}))
|
|
||||||
.ToArrayAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,24 +14,5 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
return View("Home");
|
return View("Home");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult About()
|
|
||||||
{
|
|
||||||
ViewData["Message"] = "Your application description page.";
|
|
||||||
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IActionResult Contact()
|
|
||||||
{
|
|
||||||
ViewData["Message"] = "Your contact page.";
|
|
||||||
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IActionResult Error()
|
|
||||||
{
|
|
||||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,26 +13,26 @@ using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[EnableCors("BitpayAPI")]
|
[EnableCors("BitpayAPI")]
|
||||||
[BitpayAPIConstraint]
|
[BitpayAPIConstraint]
|
||||||
|
[Authorize(Policies.CanUseStore.Key)]
|
||||||
public class InvoiceControllerAPI : Controller
|
public class InvoiceControllerAPI : Controller
|
||||||
{
|
{
|
||||||
private InvoiceController _InvoiceController;
|
private InvoiceController _InvoiceController;
|
||||||
private InvoiceRepository _InvoiceRepository;
|
private InvoiceRepository _InvoiceRepository;
|
||||||
private StoreRepository _StoreRepository;
|
|
||||||
private BTCPayNetworkProvider _NetworkProvider;
|
private BTCPayNetworkProvider _NetworkProvider;
|
||||||
|
|
||||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||||
InvoiceRepository invoceRepository,
|
InvoiceRepository invoceRepository,
|
||||||
StoreRepository storeRepository,
|
|
||||||
BTCPayNetworkProvider networkProvider)
|
BTCPayNetworkProvider networkProvider)
|
||||||
{
|
{
|
||||||
this._InvoiceController = invoiceController;
|
this._InvoiceController = invoiceController;
|
||||||
this._InvoiceRepository = invoceRepository;
|
this._InvoiceRepository = invoceRepository;
|
||||||
this._StoreRepository = storeRepository;
|
|
||||||
this._NetworkProvider = networkProvider;
|
this._NetworkProvider = networkProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,20 +41,15 @@ namespace BTCPayServer.Controllers
|
||||||
[MediaTypeConstraint("application/json")]
|
[MediaTypeConstraint("application/json")]
|
||||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||||
{
|
{
|
||||||
var store = await _StoreRepository.FindStore(this.User.GetStoreId());
|
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||||
if (store == null)
|
|
||||||
throw new BitpayHttpException(401, "Can't access to store");
|
|
||||||
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("invoices/{id}")]
|
[Route("invoices/{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||||
{
|
{
|
||||||
var store = await _StoreRepository.FindStore(this.User.GetStoreId());
|
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||||
if (store == null)
|
|
||||||
throw new BitpayHttpException(401, "Can't access to store");
|
|
||||||
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
|
|
||||||
if (invoice == null)
|
if (invoice == null)
|
||||||
throw new BitpayHttpException(404, "Object not found");
|
throw new BitpayHttpException(404, "Object not found");
|
||||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||||
|
@ -75,10 +70,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
if (dateEnd != null)
|
if (dateEnd != null)
|
||||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||||
|
|
||||||
var store = await _StoreRepository.FindStore(this.User.GetStoreId());
|
|
||||||
if (store == null)
|
|
||||||
throw new BitpayHttpException(401, "Can't access to store");
|
|
||||||
var query = new InvoiceQuery()
|
var query = new InvoiceQuery()
|
||||||
{
|
{
|
||||||
Count = limit,
|
Count = limit,
|
||||||
|
@ -88,10 +80,9 @@ namespace BTCPayServer.Controllers
|
||||||
OrderId = orderId,
|
OrderId = orderId,
|
||||||
ItemCode = itemCode,
|
ItemCode = itemCode,
|
||||||
Status = status == null ? null : new[] { status },
|
Status = status == null ? null : new[] { status },
|
||||||
StoreId = new[] { store.Id }
|
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ using BTCPayServer.Events;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
|
@ -50,14 +51,17 @@ namespace BTCPayServer.Controllers
|
||||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||||
Id = invoice.Id,
|
Id = invoice.Id,
|
||||||
Status = invoice.Status,
|
Status = invoice.Status,
|
||||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||||
|
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||||
|
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||||
|
"low",
|
||||||
RefundEmail = invoice.RefundMail,
|
RefundEmail = invoice.RefundMail,
|
||||||
CreatedDate = invoice.InvoiceTime,
|
CreatedDate = invoice.InvoiceTime,
|
||||||
ExpirationDate = invoice.ExpirationTime,
|
ExpirationDate = invoice.ExpirationTime,
|
||||||
MonitoringDate = invoice.MonitoringExpiration,
|
MonitoringDate = invoice.MonitoringExpiration,
|
||||||
OrderId = invoice.OrderId,
|
OrderId = invoice.OrderId,
|
||||||
BuyerInformation = invoice.BuyerInformation,
|
BuyerInformation = invoice.BuyerInformation,
|
||||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||||
NotificationUrl = invoice.NotificationURL,
|
NotificationUrl = invoice.NotificationURL,
|
||||||
RedirectUrl = invoice.RedirectURL,
|
RedirectUrl = invoice.RedirectURL,
|
||||||
ProductInformation = invoice.ProductInformation,
|
ProductInformation = invoice.ProductInformation,
|
||||||
|
@ -74,6 +78,7 @@ namespace BTCPayServer.Controllers
|
||||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||||
|
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
|
||||||
|
|
||||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||||
if (onchainMethod != null)
|
if (onchainMethod != null)
|
||||||
|
@ -199,6 +204,12 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||||
|
if (network == null && isDefaultCrypto)
|
||||||
|
{
|
||||||
|
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||||
|
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||||
|
paymentMethodIdStr = paymentMethodId.ToString();
|
||||||
|
}
|
||||||
if (invoice == null || network == null)
|
if (invoice == null || network == null)
|
||||||
return null;
|
return null;
|
||||||
if (!invoice.Support(paymentMethodId))
|
if (!invoice.Support(paymentMethodId))
|
||||||
|
@ -208,6 +219,7 @@ namespace BTCPayServer.Controllers
|
||||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||||
network = paymentMethodTemp.Network;
|
network = paymentMethodTemp.Network;
|
||||||
paymentMethodId = paymentMethodTemp.GetId();
|
paymentMethodId = paymentMethodTemp.GetId();
|
||||||
|
paymentMethodIdStr = paymentMethodId.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||||
|
@ -226,11 +238,13 @@ namespace BTCPayServer.Controllers
|
||||||
OrderId = invoice.OrderId,
|
OrderId = invoice.OrderId,
|
||||||
InvoiceId = invoice.Id,
|
InvoiceId = invoice.Id,
|
||||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||||
|
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
|
||||||
BtcDue = accounting.Due.ToString(),
|
BtcDue = accounting.Due.ToString(),
|
||||||
|
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||||
|
OrderAmountFiat = OrderAmountFiat(invoice.ProductInformation),
|
||||||
CustomerEmail = invoice.RefundMail,
|
CustomerEmail = invoice.RefundMail,
|
||||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||||
|
@ -278,11 +292,39 @@ namespace BTCPayServer.Controllers
|
||||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||||
{
|
{
|
||||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||||
return FormatCurrency(paymentMethod.Rate, currency);
|
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
||||||
}
|
}
|
||||||
public string FormatCurrency(decimal price, string currency)
|
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
||||||
{
|
{
|
||||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
var provider = currencies.GetNumberFormatInfo(currency);
|
||||||
|
var currencyData = currencies.GetCurrencyData(currency);
|
||||||
|
var divisibility = currencyData.Divisibility;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
|
||||||
|
if ((Math.Abs(rounded - price) / price) < 0.001m)
|
||||||
|
{
|
||||||
|
price = rounded;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
divisibility++;
|
||||||
|
}
|
||||||
|
if (divisibility != provider.CurrencyDecimalDigits)
|
||||||
|
{
|
||||||
|
provider = (NumberFormatInfo)provider.Clone();
|
||||||
|
provider.CurrencyDecimalDigits = divisibility;
|
||||||
|
}
|
||||||
|
return price.ToString("C", provider) + $" ({currency})";
|
||||||
|
}
|
||||||
|
private string OrderAmountFiat(ProductInformation productInformation)
|
||||||
|
{
|
||||||
|
// check if invoice source currency is crypto... if it is there is no "order amount in fiat"
|
||||||
|
if (_NetworkProvider.GetNetwork(productInformation.Currency) != null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
@ -355,7 +397,7 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("invoices")]
|
[Route("invoices")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[BitpayAPIConstraint(false)]
|
[BitpayAPIConstraint(false)]
|
||||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||||
{
|
{
|
||||||
|
@ -367,14 +409,19 @@ namespace BTCPayServer.Controllers
|
||||||
Count = count,
|
Count = count,
|
||||||
Skip = skip,
|
Skip = skip,
|
||||||
UserId = GetUserId(),
|
UserId = GetUserId(),
|
||||||
|
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||||
|
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||||
|
: r,
|
||||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||||
|
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
model.SearchTerm = searchTerm;
|
model.SearchTerm = searchTerm;
|
||||||
model.Invoices.Add(new InvoiceModel()
|
model.Invoices.Add(new InvoiceModel()
|
||||||
{
|
{
|
||||||
Status = invoice.Status,
|
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||||
|
ShowCheckout = invoice.Status == "new",
|
||||||
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
|
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
|
||||||
InvoiceId = invoice.Id,
|
InvoiceId = invoice.Id,
|
||||||
OrderId = invoice.OrderId ?? string.Empty,
|
OrderId = invoice.OrderId ?? string.Empty,
|
||||||
|
@ -390,11 +437,11 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("invoices/create")]
|
[Route("invoices/create")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[BitpayAPIConstraint(false)]
|
[BitpayAPIConstraint(false)]
|
||||||
public async Task<IActionResult> CreateInvoice()
|
public async Task<IActionResult> CreateInvoice()
|
||||||
{
|
{
|
||||||
var stores = await GetStores(GetUserId());
|
var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
|
||||||
if (stores.Count() == 0)
|
if (stores.Count() == 0)
|
||||||
{
|
{
|
||||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||||
|
@ -405,18 +452,23 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("invoices/create")]
|
[Route("invoices/create")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[BitpayAPIConstraint(false)]
|
[BitpayAPIConstraint(false)]
|
||||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||||
{
|
{
|
||||||
model.Stores = await GetStores(GetUserId(), model.StoreId);
|
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||||
|
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||||
|
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||||
|
if (store == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
|
||||||
|
}
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
|
||||||
StatusMessage = null;
|
StatusMessage = null;
|
||||||
if (store.Role != StoreRoles.Owner)
|
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
||||||
return View(model);
|
return View(model);
|
||||||
|
@ -454,20 +506,15 @@ 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 ex)
|
||||||
{
|
{
|
||||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SelectList> GetStores(string userId, string storeId = null)
|
|
||||||
{
|
|
||||||
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[BitpayAPIConstraint(false)]
|
[BitpayAPIConstraint(false)]
|
||||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||||
{
|
{
|
||||||
|
@ -481,7 +528,7 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("invoices/invalidatepaid")]
|
[Route("invoices/invalidatepaid")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[BitpayAPIConstraint(false)]
|
[BitpayAPIConstraint(false)]
|
||||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers
|
||||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||||
|
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||||
//Another way of passing buyer info to support
|
//Another way of passing buyer info to support
|
||||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||||
|
@ -111,6 +113,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 +138,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 +203,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 +211,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,16 +247,14 @@ 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
|
|
||||||
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
|
|
||||||
|
|
||||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
|
|
||||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException(errorMessage);
|
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
|
||||||
|
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||||
|
{
|
||||||
|
throw new PaymentMethodUnavailableException(errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
///////////////
|
///////////////
|
||||||
|
@ -243,19 +271,13 @@ 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)
|
||||||
return defaultPolicy;
|
return defaultPolicy;
|
||||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||||
|
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
|
||||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||||
mappings.Add("high", SpeedPolicy.HighSpeed);
|
mappings.Add("high", SpeedPolicy.HighSpeed);
|
||||||
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
||||||
|
|
|
@ -21,10 +21,11 @@ using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[Route("[controller]/[action]")]
|
[Route("[controller]/[action]")]
|
||||||
public class ManageController : Controller
|
public class ManageController : Controller
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,101 @@ 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);
|
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
var result = await GetRates2(currencyPairs, storeId);
|
||||||
if(rates == null)
|
var rates = (result as JsonResult)?.Value as Rate[];
|
||||||
|
if (rates == null)
|
||||||
return result;
|
return result;
|
||||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
return Json(new DataWrapper<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);
|
var store = this.HttpContext.GetStoreData();
|
||||||
if (rateProvider == null)
|
if(store == null || store.Id != storeId)
|
||||||
return NotFound();
|
store = await _StoreRepo.FindStore(storeId);
|
||||||
|
if (store == null)
|
||||||
|
{
|
||||||
|
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||||
|
result.StatusCode = 404;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||||
|
|
||||||
var allRates = (await rateProvider.GetRatesAsync());
|
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
|
||||||
return Json(allRates.Select(r =>
|
foreach(var currency in currencyPairs.Split(','))
|
||||||
new NBitpayClient.Rate()
|
{
|
||||||
|
if(!CurrencyPair.TryParse(currency, out var pair))
|
||||||
|
{
|
||||||
|
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||||
|
result.StatusCode = 400;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
pairs.Add(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||||
|
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||||
|
return Json(pairs
|
||||||
|
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
|
||||||
|
.Where(r => r.Value.HasValue)
|
||||||
|
.Select(r =>
|
||||||
|
new Rate()
|
||||||
{
|
{
|
||||||
Code = r.Currency,
|
CryptoCode = r.Pair.Left,
|
||||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
Code = r.Pair.Right,
|
||||||
Value = r.Value
|
CurrencyPair = r.Pair.ToString(),
|
||||||
|
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 = "currencyPair")]
|
||||||
|
public string CurrencyPair
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "code")]
|
||||||
|
public string Code
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
[JsonProperty(PropertyName = "rate")]
|
||||||
|
public decimal Value
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,15 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Authorize(Roles = Roles.ServerAdmin)]
|
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||||
public class ServerController : Controller
|
public class ServerController : Controller
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
@ -241,10 +241,13 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
if (command == "Test")
|
if (command == "Test")
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
|
||||||
return View(model);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if(!model.Settings.IsComplete())
|
||||||
|
{
|
||||||
|
model.StatusMessage = "Error: Required fields missing";
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
var client = model.Settings.CreateSmtpClient();
|
var client = model.Settings.CreateSmtpClient();
|
||||||
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||||
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||||
|
@ -255,11 +258,8 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
else
|
else // if(command == "Save")
|
||||||
{
|
{
|
||||||
ModelState.Remove(nameof(model.TestEmail));
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
return View(model);
|
|
||||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||||
model.StatusMessage = "Email settings saved";
|
model.StatusMessage = "Email settings saved";
|
||||||
return View(model);
|
return View(model);
|
||||||
|
|
|
@ -10,6 +10,7 @@ using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using LedgerWallet;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
|
@ -21,9 +22,9 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
|
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||||
|
@ -60,7 +61,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
vm.ServerUrl = GetStoreUrl(storeId);
|
vm.ServerUrl = GetStoreUrl(storeId);
|
||||||
vm.CryptoCode = cryptoCode;
|
vm.CryptoCode = cryptoCode;
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
@ -188,7 +189,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
var strategy = GetDirectDerivationStrategy(store, network);
|
var strategy = GetDirectDerivationStrategy(store, network);
|
||||||
var strategyBase = GetDerivationStrategy(store, network);
|
var strategyBase = GetDerivationStrategy(store, network);
|
||||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
if (strategy == null || await hw.GetKeyPath(network, strategy) == null)
|
||||||
{
|
{
|
||||||
throw new Exception($"This store is not configured to use this ledger");
|
throw new Exception($"This store is not configured to use this ledger");
|
||||||
}
|
}
|
||||||
|
@ -286,11 +287,76 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||||
var changeAddress = await change;
|
var changeAddress = await change;
|
||||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
var send = new[] { (
|
||||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
destination: destinationAddress as IDestination,
|
||||||
feeRateValue,
|
amount: amountBTC,
|
||||||
changeAddress.Item1,
|
substractFees: subsctractFeesValue) };
|
||||||
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
|
|
||||||
|
foreach (var element in send)
|
||||||
|
{
|
||||||
|
if (element.destination == null)
|
||||||
|
throw new ArgumentNullException(nameof(element.destination));
|
||||||
|
if (element.amount == null)
|
||||||
|
throw new ArgumentNullException(nameof(element.amount));
|
||||||
|
if (element.amount <= Money.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundKeyPath = await hw.GetKeyPath(network, strategy);
|
||||||
|
if (foundKeyPath == null)
|
||||||
|
{
|
||||||
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionBuilder builder = new TransactionBuilder();
|
||||||
|
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||||
|
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||||
|
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||||
|
|
||||||
|
foreach (var element in send)
|
||||||
|
{
|
||||||
|
builder.Send(element.destination, element.amount);
|
||||||
|
if (element.substractFees)
|
||||||
|
builder.SubtractFees();
|
||||||
|
}
|
||||||
|
builder.SetChange(changeAddress.Item1);
|
||||||
|
builder.SendEstimatedFees(feeRateValue);
|
||||||
|
builder.Shuffle();
|
||||||
|
var unsigned = builder.BuildTransaction(false);
|
||||||
|
|
||||||
|
var keypaths = new Dictionary<Script, KeyPath>();
|
||||||
|
foreach (var c in unspentCoins)
|
||||||
|
{
|
||||||
|
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasChange = unsigned.Outputs.Count == 2;
|
||||||
|
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||||
|
|
||||||
|
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||||
|
|
||||||
|
if(!strategy.Segwit)
|
||||||
|
{
|
||||||
|
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||||
|
var explorer = _ExplorerProvider.GetExplorerClient(network);
|
||||||
|
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||||
|
foreach(var getTransactionAsync in getTransactionAsyncs)
|
||||||
|
{
|
||||||
|
var tx = (await getTransactionAsync.Op);
|
||||||
|
if(tx == null)
|
||||||
|
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||||
|
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||||
|
{
|
||||||
|
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||||
|
InputCoin = c,
|
||||||
|
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||||
|
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||||
|
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||||
|
@ -336,8 +402,6 @@ namespace BTCPayServer.Controllers
|
||||||
var directStrategy = strategy as DirectDerivationStrategy;
|
var directStrategy = strategy as DirectDerivationStrategy;
|
||||||
if (directStrategy == null)
|
if (directStrategy == null)
|
||||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||||
if (!directStrategy.Segwit)
|
|
||||||
return null;
|
|
||||||
return directStrategy;
|
return directStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||||
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
public IActionResult AddLightningNode(string storeId, string cryptoCode)
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||||
|
@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||||
{
|
{
|
||||||
vm.CryptoCode = cryptoCode;
|
vm.CryptoCode = cryptoCode;
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||||
|
|
|
@ -4,6 +4,8 @@ using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
@ -19,6 +21,7 @@ using NBitcoin.DataEncoders;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -27,11 +30,12 @@ using System.Threading.Tasks;
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Route("stores")]
|
[Route("stores")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[Authorize(Policy = StorePolicies.OwnStore)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
|
||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public partial class StoresController : Controller
|
public partial class StoresController : Controller
|
||||||
{
|
{
|
||||||
|
BTCPayRateProviderFactory _RateFactory;
|
||||||
public string CreatedStoreId { get; set; }
|
public string CreatedStoreId { get; set; }
|
||||||
public StoresController(
|
public StoresController(
|
||||||
NBXplorerDashboard dashboard,
|
NBXplorerDashboard dashboard,
|
||||||
|
@ -45,12 +49,13 @@ namespace BTCPayServer.Controllers
|
||||||
AccessTokenController tokenController,
|
AccessTokenController tokenController,
|
||||||
BTCPayWalletProvider walletProvider,
|
BTCPayWalletProvider walletProvider,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
|
BTCPayRateProviderFactory rateFactory,
|
||||||
ExplorerClientProvider explorerProvider,
|
ExplorerClientProvider explorerProvider,
|
||||||
IFeeProviderFactory feeRateProvider,
|
IFeeProviderFactory feeRateProvider,
|
||||||
LanguageService langService,
|
LanguageService langService,
|
||||||
IHostingEnvironment env,
|
IHostingEnvironment env)
|
||||||
CoinAverageSettings coinAverage)
|
|
||||||
{
|
{
|
||||||
|
_RateFactory = rateFactory;
|
||||||
_Dashboard = dashboard;
|
_Dashboard = dashboard;
|
||||||
_Repo = repo;
|
_Repo = repo;
|
||||||
_TokenRepository = tokenRepo;
|
_TokenRepository = tokenRepo;
|
||||||
|
@ -66,9 +71,7 @@ namespace BTCPayServer.Controllers
|
||||||
_ServiceProvider = serviceProvider;
|
_ServiceProvider = serviceProvider;
|
||||||
_BtcpayServerOptions = btcpayServerOptions;
|
_BtcpayServerOptions = btcpayServerOptions;
|
||||||
_BTCPayEnv = btcpayEnv;
|
_BTCPayEnv = btcpayEnv;
|
||||||
_CoinAverage = coinAverage;
|
|
||||||
}
|
}
|
||||||
CoinAverageSettings _CoinAverage;
|
|
||||||
NBXplorerDashboard _Dashboard;
|
NBXplorerDashboard _Dashboard;
|
||||||
BTCPayServerOptions _BtcpayServerOptions;
|
BTCPayServerOptions _BtcpayServerOptions;
|
||||||
BTCPayServerEnvironment _BTCPayEnv;
|
BTCPayServerEnvironment _BTCPayEnv;
|
||||||
|
@ -93,13 +96,10 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
public IActionResult Wallet(string cryptoCode)
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
WalletModel model = new WalletModel();
|
WalletModel model = new WalletModel();
|
||||||
model.ServerUrl = GetStoreUrl(storeId);
|
model.ServerUrl = GetStoreUrl(StoreData.Id);
|
||||||
model.CryptoCurrency = cryptoCode;
|
model.CryptoCurrency = cryptoCode;
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
@ -111,17 +111,17 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/users")]
|
[Route("{storeId}/users")]
|
||||||
public async Task<IActionResult> StoreUsers(string storeId)
|
public async Task<IActionResult> StoreUsers()
|
||||||
{
|
{
|
||||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||||
await FillUsers(storeId, vm);
|
await FillUsers(vm);
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
private async Task FillUsers(StoreUsersViewModel vm)
|
||||||
{
|
{
|
||||||
var users = await _Repo.GetStoreUsers(storeId);
|
var users = await _Repo.GetStoreUsers(StoreData.Id);
|
||||||
vm.StoreId = storeId;
|
vm.StoreId = StoreData.Id;
|
||||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||||
{
|
{
|
||||||
Email = u.Email,
|
Email = u.Email,
|
||||||
|
@ -130,11 +130,20 @@ namespace BTCPayServer.Controllers
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StoreData StoreData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return this.HttpContext.GetStoreData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{storeId}/users")]
|
[Route("{storeId}/users")]
|
||||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
|
||||||
{
|
{
|
||||||
await FillUsers(storeId, vm);
|
await FillUsers(vm);
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
@ -150,7 +159,7 @@ namespace BTCPayServer.Controllers
|
||||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
if (!await _Repo.AddStoreUser(StoreData.Id, user.Id, vm.Role))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
@ -161,19 +170,16 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/users/{userId}/delete")]
|
[Route("{storeId}/users/{userId}/delete")]
|
||||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
public async Task<IActionResult> DeleteStoreUser(string userId)
|
||||||
{
|
{
|
||||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||||
var store = await _Repo.FindStore(storeId, userId);
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
var user = await _UserManager.FindByIdAsync(userId);
|
var user = await _UserManager.FindByIdAsync(userId);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return View("Confirm", new ConfirmModel()
|
return View("Confirm", new ConfirmModel()
|
||||||
{
|
{
|
||||||
Title = $"Remove store user",
|
Title = $"Remove store user",
|
||||||
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
|
Description = $"Are you sure to remove access to remove access to {user.Email}?",
|
||||||
Action = "Delete"
|
Action = "Delete"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -188,15 +194,151 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/checkout")]
|
[Route("{storeId}/rates")]
|
||||||
public async Task<IActionResult> CheckoutExperience(string storeId)
|
public IActionResult Rates()
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var storeBlob = StoreData.GetStoreBlob();
|
||||||
if (store == null)
|
var vm = new RatesViewModel();
|
||||||
return NotFound();
|
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||||
var storeBlob = store.GetStoreBlob();
|
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||||
|
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||||
|
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||||
|
vm.AvailableExchanges = GetSupportedExchanges();
|
||||||
|
vm.ShowScripting = storeBlob.RateScripting;
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/rates")]
|
||||||
|
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||||
|
{
|
||||||
|
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
if (model.PreferredExchange != null)
|
||||||
|
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
var blob = StoreData.GetStoreBlob();
|
||||||
|
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||||
|
model.AvailableExchanges = GetSupportedExchanges();
|
||||||
|
|
||||||
|
blob.PreferredExchange = model.PreferredExchange;
|
||||||
|
blob.SetRateMultiplier(model.RateMultiplier);
|
||||||
|
|
||||||
|
if (!model.ShowScripting)
|
||||||
|
{
|
||||||
|
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RateRules rules = null;
|
||||||
|
if (model.ShowScripting)
|
||||||
|
{
|
||||||
|
if (!RateRules.TryParse(model.Script, out rules, out var errors))
|
||||||
|
{
|
||||||
|
errors = errors ?? new List<RateRulesErrors>();
|
||||||
|
var errorString = String.Join(", ", errors.ToArray());
|
||||||
|
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
blob.RateScript = rules.ToString();
|
||||||
|
ModelState.Remove(nameof(model.Script));
|
||||||
|
model.Script = blob.RateScript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rules = blob.GetRateRules(_NetworkProvider);
|
||||||
|
|
||||||
|
if (command == "Test")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(model.ScriptTest))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
var pairs = new List<CurrencyPair>();
|
||||||
|
foreach (var pair in splitted)
|
||||||
|
{
|
||||||
|
if (!CurrencyPair.TryParse(pair, out var currencyPair))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
pairs.Add(currencyPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||||
|
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||||
|
foreach (var fetch in fetchs)
|
||||||
|
{
|
||||||
|
var testResult = await (fetch.Value);
|
||||||
|
testResults.Add(new RatesViewModel.TestResultViewModel()
|
||||||
|
{
|
||||||
|
CurrencyPair = fetch.Key.ToString(),
|
||||||
|
Error = testResult.Errors.Count != 0,
|
||||||
|
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: testResult.EvaluatedRule
|
||||||
|
});
|
||||||
|
}
|
||||||
|
model.TestRateRules = testResults;
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
else // command == Save
|
||||||
|
{
|
||||||
|
if (StoreData.SetStoreBlob(blob))
|
||||||
|
{
|
||||||
|
await _Repo.UpdateStore(StoreData);
|
||||||
|
StatusMessage = "Rate settings updated";
|
||||||
|
}
|
||||||
|
return RedirectToAction(nameof(Rates), new
|
||||||
|
{
|
||||||
|
storeId = StoreData.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/rates/confirm")]
|
||||||
|
public IActionResult ShowRateRules(bool scripting)
|
||||||
|
{
|
||||||
|
return View("Confirm", new ConfirmModel()
|
||||||
|
{
|
||||||
|
Action = "Continue",
|
||||||
|
Title = "Rate rule scripting",
|
||||||
|
Description = scripting ?
|
||||||
|
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||||
|
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||||
|
ButtonClass = "btn-primary"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/rates/confirm")]
|
||||||
|
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||||
|
{
|
||||||
|
var blob = StoreData.GetStoreBlob();
|
||||||
|
blob.RateScripting = scripting;
|
||||||
|
blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||||
|
StoreData.SetStoreBlob(blob);
|
||||||
|
await _Repo.UpdateStore(StoreData);
|
||||||
|
StatusMessage = "Rate rules scripting activated";
|
||||||
|
return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/checkout")]
|
||||||
|
public IActionResult CheckoutExperience()
|
||||||
|
{
|
||||||
|
var storeBlob = StoreData.GetStoreBlob();
|
||||||
var vm = new CheckoutExperienceViewModel();
|
var vm = new CheckoutExperienceViewModel();
|
||||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
|
||||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||||
|
@ -204,12 +346,13 @@ namespace BTCPayServer.Controllers
|
||||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||||
|
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{storeId}/checkout")]
|
[Route("{storeId}/checkout")]
|
||||||
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
|
public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
|
||||||
{
|
{
|
||||||
CurrencyValue lightningMaxValue = null;
|
CurrencyValue lightningMaxValue = null;
|
||||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||||
|
@ -228,16 +371,12 @@ namespace BTCPayServer.Controllers
|
||||||
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
|
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
bool needUpdate = false;
|
bool needUpdate = false;
|
||||||
var blob = store.GetStoreBlob();
|
var blob = StoreData.GetStoreBlob();
|
||||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||||
}
|
}
|
||||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||||
model.SetLanguages(_LangService, model.DefaultLang);
|
model.SetLanguages(_LangService, model.DefaultLang);
|
||||||
|
@ -253,33 +392,33 @@ namespace BTCPayServer.Controllers
|
||||||
blob.OnChainMinValue = onchainMinValue;
|
blob.OnChainMinValue = onchainMinValue;
|
||||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||||
if (store.SetStoreBlob(blob))
|
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||||
|
if (StoreData.SetStoreBlob(blob))
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
}
|
}
|
||||||
if (needUpdate)
|
if (needUpdate)
|
||||||
{
|
{
|
||||||
await _Repo.UpdateStore(store);
|
await _Repo.UpdateStore(StoreData);
|
||||||
StatusMessage = "Store successfully updated";
|
StatusMessage = "Store successfully updated";
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(CheckoutExperience), new
|
return RedirectToAction(nameof(CheckoutExperience), new
|
||||||
{
|
{
|
||||||
storeId = storeId
|
storeId = StoreData.Id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}")]
|
[Route("{storeId}")]
|
||||||
public async Task<IActionResult> UpdateStore(string storeId)
|
public IActionResult UpdateStore()
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
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.Id = store.Id;
|
vm.Id = store.Id;
|
||||||
vm.StoreName = store.StoreName;
|
vm.StoreName = store.StoreName;
|
||||||
vm.StoreWebsite = store.StoreWebsite;
|
vm.StoreWebsite = store.StoreWebsite;
|
||||||
|
@ -288,8 +427,8 @@ namespace BTCPayServer.Controllers
|
||||||
AddPaymentMethods(store, vm);
|
AddPaymentMethods(store, vm);
|
||||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
|
||||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||||
|
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,81 +468,57 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{storeId}")]
|
[Route("{storeId}")]
|
||||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
public async Task<IActionResult> UpdateStore(StoreViewModel model)
|
||||||
{
|
{
|
||||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
AddPaymentMethods(StoreData, model);
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
if (model.PreferredExchange != null)
|
|
||||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
AddPaymentMethods(store, model);
|
|
||||||
|
|
||||||
bool needUpdate = false;
|
bool needUpdate = false;
|
||||||
if (store.SpeedPolicy != model.SpeedPolicy)
|
if (StoreData.SpeedPolicy != model.SpeedPolicy)
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
store.SpeedPolicy = model.SpeedPolicy;
|
StoreData.SpeedPolicy = model.SpeedPolicy;
|
||||||
}
|
}
|
||||||
if (store.StoreName != model.StoreName)
|
if (StoreData.StoreName != model.StoreName)
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
store.StoreName = model.StoreName;
|
StoreData.StoreName = model.StoreName;
|
||||||
}
|
}
|
||||||
if (store.StoreWebsite != model.StoreWebsite)
|
if (StoreData.StoreWebsite != model.StoreWebsite)
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
store.StoreWebsite = model.StoreWebsite;
|
StoreData.StoreWebsite = model.StoreWebsite;
|
||||||
}
|
}
|
||||||
|
|
||||||
var blob = store.GetStoreBlob();
|
var blob = StoreData.GetStoreBlob();
|
||||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||||
|
blob.PaymentTolerance = model.PaymentTolerance;
|
||||||
|
|
||||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
if (StoreData.SetStoreBlob(blob))
|
||||||
blob.PreferredExchange = model.PreferredExchange;
|
|
||||||
|
|
||||||
blob.SetRateMultiplier(model.RateMultiplier);
|
|
||||||
|
|
||||||
if (store.SetStoreBlob(blob))
|
|
||||||
{
|
{
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needUpdate)
|
if (needUpdate)
|
||||||
{
|
{
|
||||||
await _Repo.UpdateStore(store);
|
await _Repo.UpdateStore(StoreData);
|
||||||
StatusMessage = "Store successfully updated";
|
StatusMessage = "Store successfully updated";
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(UpdateStore), new
|
return RedirectToAction(nameof(UpdateStore), new
|
||||||
{
|
{
|
||||||
storeId = storeId
|
storeId = StoreData.Id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private (String DisplayName, String Name)[] GetSupportedExchanges()
|
private CoinAverageExchange[] GetSupportedExchanges()
|
||||||
{
|
{
|
||||||
return new[] { ("Coin Average", "coinaverage") }
|
return _RateFactory.GetSupportedExchanges()
|
||||||
.Concat(_CoinAverage.AvailableExchanges)
|
.Select(c => c.Value)
|
||||||
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||||
|
@ -415,10 +530,10 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/Tokens")]
|
[Route("{storeId}/Tokens")]
|
||||||
public async Task<IActionResult> ListTokens(string storeId)
|
public async Task<IActionResult> ListTokens()
|
||||||
{
|
{
|
||||||
var model = new TokensViewModel();
|
var model = new TokensViewModel();
|
||||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId);
|
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
|
||||||
model.StatusMessage = StatusMessage;
|
model.StatusMessage = StatusMessage;
|
||||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||||
{
|
{
|
||||||
|
@ -427,30 +542,43 @@ namespace BTCPayServer.Controllers
|
||||||
SIN = t.SIN,
|
SIN = t.SIN,
|
||||||
Id = t.Value
|
Id = t.Value
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
|
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(StoreData.Id)).FirstOrDefault();
|
||||||
|
if (model.ApiKey == null)
|
||||||
|
model.EncodedApiKey = "*API Key*";
|
||||||
|
else
|
||||||
|
model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("/api-tokens")]
|
[Route("/api-tokens")]
|
||||||
[Route("{storeId}/Tokens/Create")]
|
[Route("{storeId}/Tokens/Create")]
|
||||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> CreateToken(CreateTokenViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
model.Label = model.Label ?? String.Empty;
|
model.Label = model.Label ?? String.Empty;
|
||||||
storeId = model.StoreId ?? storeId;
|
|
||||||
var userId = GetUserId();
|
var userId = GetUserId();
|
||||||
if (userId == null)
|
if (userId == null)
|
||||||
return Unauthorized();
|
return Challenge(Policies.CookieAuthentication);
|
||||||
var store = await _Repo.FindStore(storeId, userId);
|
|
||||||
if (store == null)
|
var store = StoreData;
|
||||||
return Unauthorized();
|
var storeId = StoreData?.Id;
|
||||||
if (store.Role != StoreRoles.Owner)
|
if (storeId == null)
|
||||||
{
|
{
|
||||||
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
|
storeId = model.StoreId;
|
||||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
store = await _Repo.FindStore(storeId, userId);
|
||||||
|
if (store == null)
|
||||||
|
return Challenge(Policies.CookieAuthentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||||
|
{
|
||||||
|
return Challenge(Policies.CookieAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenRequest = new TokenRequest()
|
var tokenRequest = new TokenRequest()
|
||||||
|
@ -491,11 +619,20 @@ namespace BTCPayServer.Controllers
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("/api-tokens")]
|
[Route("/api-tokens")]
|
||||||
[Route("{storeId}/Tokens/Create")]
|
[Route("{storeId}/Tokens/Create")]
|
||||||
public async Task<IActionResult> CreateToken(string storeId)
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> CreateToken()
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = GetUserId();
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
return Unauthorized();
|
return Challenge(Policies.CookieAuthentication);
|
||||||
|
var storeId = StoreData?.Id;
|
||||||
|
if (StoreData != null)
|
||||||
|
{
|
||||||
|
if (!StoreData.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||||
|
{
|
||||||
|
return Challenge(Policies.CookieAuthentication);
|
||||||
|
}
|
||||||
|
}
|
||||||
var model = new CreateTokenViewModel();
|
var model = new CreateTokenViewModel();
|
||||||
model.Facade = "merchant";
|
model.Facade = "merchant";
|
||||||
ViewBag.HidePublicKey = storeId == null;
|
ViewBag.HidePublicKey = storeId == null;
|
||||||
|
@ -504,20 +641,25 @@ namespace BTCPayServer.Controllers
|
||||||
model.StoreId = storeId;
|
model.StoreId = storeId;
|
||||||
if (storeId == null)
|
if (storeId == null)
|
||||||
{
|
{
|
||||||
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
var stores = await _Repo.GetStoresByUserId(userId);
|
||||||
|
model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||||
|
if (model.Stores.Count() == 0)
|
||||||
|
{
|
||||||
|
StatusMessage = "Error: You need to be owner of at least one store before pairing";
|
||||||
|
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{storeId}/Tokens/Delete")]
|
[Route("{storeId}/Tokens/Delete")]
|
||||||
public async Task<IActionResult> DeleteToken(string storeId, string tokenId)
|
public async Task<IActionResult> DeleteToken(string tokenId)
|
||||||
{
|
{
|
||||||
var token = await _TokenRepository.GetToken(tokenId);
|
var token = await _TokenRepository.GetToken(tokenId);
|
||||||
if (token == null ||
|
if (token == null ||
|
||||||
token.StoreId != storeId ||
|
token.StoreId != StoreData.Id ||
|
||||||
!await _TokenRepository.DeleteToken(tokenId))
|
!await _TokenRepository.DeleteToken(tokenId))
|
||||||
StatusMessage = "Failure to revoke this token";
|
StatusMessage = "Failure to revoke this token";
|
||||||
else
|
else
|
||||||
|
@ -525,11 +667,26 @@ namespace BTCPayServer.Controllers
|
||||||
return RedirectToAction(nameof(ListTokens));
|
return RedirectToAction(nameof(ListTokens));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/tokens/apikey")]
|
||||||
|
public async Task<IActionResult> GenerateAPIKey()
|
||||||
|
{
|
||||||
|
var store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
await _TokenRepository.GenerateLegacyAPIKey(StoreData.Id);
|
||||||
|
StatusMessage = "API Key re-generated";
|
||||||
|
return RedirectToAction(nameof(ListTokens));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("/api-access-request")]
|
[Route("/api-access-request")]
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
||||||
{
|
{
|
||||||
|
var userId = GetUserId();
|
||||||
|
if (userId == null)
|
||||||
|
return Challenge(Policies.CookieAuthentication);
|
||||||
if (pairingCode == null)
|
if (pairingCode == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||||
|
@ -540,7 +697,7 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
var stores = await _Repo.GetStoresByUserId(userId);
|
||||||
return View(new PairingModel()
|
return View(new PairingModel()
|
||||||
{
|
{
|
||||||
Id = pairing.Id,
|
Id = pairing.Id,
|
||||||
|
@ -548,7 +705,7 @@ namespace BTCPayServer.Controllers
|
||||||
Label = pairing.Label,
|
Label = pairing.Label,
|
||||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||||
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
|
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||||
Stores = stores.Select(s => new PairingModel.StoreViewModel()
|
Stores = stores.Where(u => u.HasClaim(Policies.CanModifyStoreSettings.Key)).Select(s => new PairingModel.StoreViewModel()
|
||||||
{
|
{
|
||||||
Id = s.Id,
|
Id = s.Id,
|
||||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||||
|
@ -559,19 +716,22 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("/api-access-request")]
|
[Route("/api-access-request")]
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||||
{
|
{
|
||||||
if (pairingCode == null)
|
if (pairingCode == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var store = await _Repo.FindStore(selectedStore, GetUserId());
|
var userId = GetUserId();
|
||||||
|
if (userId == null)
|
||||||
|
return Challenge(Policies.CookieAuthentication);
|
||||||
|
var store = await _Repo.FindStore(selectedStore, userId);
|
||||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||||
if (store == null || pairing == null)
|
if (store == null || pairing == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
if (store.Role != StoreRoles.Owner)
|
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||||
{
|
{
|
||||||
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
|
return Challenge(Policies.CookieAuthentication);
|
||||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||||
|
@ -597,6 +757,8 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
private string GetUserId()
|
private string GetUserId()
|
||||||
{
|
{
|
||||||
|
if (User.Identity.AuthenticationType != Policies.CookieAuthentication)
|
||||||
|
return null;
|
||||||
return _UserManager.GetUserId(User);
|
return _UserManager.GetUserId(User);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -15,7 +16,7 @@ using NBXplorer.DerivationStrategy;
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Route("stores")]
|
[Route("stores")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public partial class UserStoresController : Controller
|
public partial class UserStoresController : Controller
|
||||||
{
|
{
|
||||||
|
@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/delete")]
|
[Route("{storeId}/delete")]
|
||||||
public async Task<IActionResult> DeleteStore(string storeId)
|
public IActionResult DeleteStore(string storeId)
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return View("Confirm", new ConfirmModel()
|
return View("Confirm", new ConfirmModel()
|
||||||
|
@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers
|
||||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = GetUserId();
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
await _Repo.RemoveStore(storeId, userId);
|
await _Repo.RemoveStore(storeId, userId);
|
||||||
|
@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers
|
||||||
Id = store.Id,
|
Id = store.Id,
|
||||||
Name = store.StoreName,
|
Name = store.StoreName,
|
||||||
WebSite = store.StoreWebsite,
|
WebSite = store.StoreWebsite,
|
||||||
IsOwner = store.Role == StoreRoles.Owner,
|
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
|
||||||
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return View(result);
|
return View(result);
|
||||||
|
|
23
BTCPayServer/Data/APIKeyData.cs
Normal file
23
BTCPayServer/Data/APIKeyData.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data
|
||||||
|
{
|
||||||
|
public class APIKeyData
|
||||||
|
{
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string StoreId
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,6 +86,11 @@ namespace BTCPayServer.Data
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DbSet<APIKeyData> ApiKeys
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||||
|
@ -112,6 +117,8 @@ namespace BTCPayServer.Data
|
||||||
t.StoreDataId
|
t.StoreDataId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Entity<APIKeyData>()
|
||||||
|
.HasIndex(o => o.StoreId);
|
||||||
|
|
||||||
builder.Entity<AppData>()
|
builder.Entity<AppData>()
|
||||||
.HasOne(a => a.StoreData);
|
.HasOne(a => a.StoreData);
|
||||||
|
|
|
@ -41,10 +41,12 @@ namespace BTCPayServer.Data
|
||||||
|
|
||||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||||
{
|
{
|
||||||
if (_Type == DatabaseType.Sqlite)
|
builder.UseMemoryStorage();
|
||||||
builder.UseMemoryStorage(); //Sql provider does not support multiple workers
|
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
|
||||||
else if (_Type == DatabaseType.Postgres)
|
//if (_Type == DatabaseType.Sqlite)
|
||||||
builder.UsePostgreSqlStorage(_ConnectionString);
|
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
|
||||||
|
//else if (_Type == DatabaseType.Postgres)
|
||||||
|
// builder.UsePostgreSqlStorage(_ConnectionString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ using Newtonsoft.Json.Linq;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.JsonConverters;
|
using BTCPayServer.JsonConverters;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
|
@ -120,7 +125,7 @@ namespace BTCPayServer.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||||
{
|
{
|
||||||
DerivationStrategy = null;
|
DerivationStrategy = null;
|
||||||
}
|
}
|
||||||
|
@ -151,10 +156,35 @@ namespace BTCPayServer.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
|
[Obsolete]
|
||||||
public string Role
|
public string Role
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Claim[] GetClaims()
|
||||||
|
{
|
||||||
|
List<Claim> claims = new List<Claim>();
|
||||||
|
#pragma warning disable CS0612 // Type or member is obsolete
|
||||||
|
var role = Role;
|
||||||
|
#pragma warning restore CS0612 // Type or member is obsolete
|
||||||
|
if (role == StoreRoles.Owner)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
|
||||||
|
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||||
|
}
|
||||||
|
if (role == StoreRoles.Guest)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||||
|
}
|
||||||
|
return claims.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasClaim(string claim)
|
||||||
|
{
|
||||||
|
return GetClaims().Any(c => c.Type == claim);
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] StoreBlob
|
public byte[] StoreBlob
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
|
@ -178,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)
|
||||||
|
@ -192,9 +225,9 @@ namespace BTCPayServer.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RateRule
|
public class RateRule_Obsolete
|
||||||
{
|
{
|
||||||
public RateRule()
|
public RateRule_Obsolete()
|
||||||
{
|
{
|
||||||
RuleName = "Multiplier";
|
RuleName = "Multiplier";
|
||||||
}
|
}
|
||||||
|
@ -214,6 +247,7 @@ namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
InvoiceExpiration = 15;
|
InvoiceExpiration = 15;
|
||||||
MonitoringExpiration = 60;
|
MonitoringExpiration = 60;
|
||||||
|
PaymentTolerance = 0;
|
||||||
RequiresRefundEmail = true;
|
RequiresRefundEmail = true;
|
||||||
}
|
}
|
||||||
public bool NetworkFeeDisabled
|
public bool NetworkFeeDisabled
|
||||||
|
@ -246,8 +280,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()
|
||||||
{
|
{
|
||||||
|
@ -261,7 +295,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))]
|
||||||
|
@ -273,6 +307,11 @@ namespace BTCPayServer.Data
|
||||||
public Uri CustomLogo { get; set; }
|
public Uri CustomLogo { get; set; }
|
||||||
[JsonConverter(typeof(UriJsonConverter))]
|
[JsonConverter(typeof(UriJsonConverter))]
|
||||||
public Uri CustomCSS { get; set; }
|
public Uri CustomCSS { get; set; }
|
||||||
|
public string HtmlTitle { get; set; }
|
||||||
|
|
||||||
|
public bool RateScripting { get; set; }
|
||||||
|
|
||||||
|
public string RateScript { get; set; }
|
||||||
|
|
||||||
|
|
||||||
string _LightningDescriptionTemplate;
|
string _LightningDescriptionTemplate;
|
||||||
|
@ -288,12 +327,48 @@ namespace BTCPayServer.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RateRules GetRateRules()
|
[DefaultValue(0)]
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||||
|
public double PaymentTolerance { get; set; }
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,21 +6,6 @@ using BTCPayServer.HostedServices;
|
||||||
|
|
||||||
namespace BTCPayServer.Events
|
namespace BTCPayServer.Events
|
||||||
{
|
{
|
||||||
public class NBXplorerErrorEvent
|
|
||||||
{
|
|
||||||
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
|
|
||||||
{
|
|
||||||
Message = errorMessage;
|
|
||||||
Network = network;
|
|
||||||
}
|
|
||||||
public string Message { get; set; }
|
|
||||||
public BTCPayNetwork Network { get; set; }
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public class NBXplorerStateChangedEvent
|
public class NBXplorerStateChangedEvent
|
||||||
{
|
{
|
||||||
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
||||||
|
|
|
@ -30,6 +30,7 @@ using BTCPayServer.Models;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
{
|
{
|
||||||
|
@ -103,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();
|
||||||
|
@ -134,6 +129,14 @@ namespace BTCPayServer
|
||||||
request.PathBase.ToUriComponent());
|
request.PathBase.ToUriComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||||
|
{
|
||||||
|
bool isRelative =
|
||||||
|
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||||
|
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||||
|
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||||
{
|
{
|
||||||
services.Configure<BTCPayServerOptions>(o =>
|
services.Configure<BTCPayServerOptions>(o =>
|
||||||
|
@ -153,19 +156,49 @@ namespace BTCPayServer
|
||||||
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
|
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
|
||||||
|
{
|
||||||
|
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
|
||||||
|
{
|
||||||
|
foreach(var item in items)
|
||||||
|
{
|
||||||
|
hashSet.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static bool GetIsBitpayAPI(this HttpContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
|
||||||
|
obj is bool b && b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value)
|
||||||
|
{
|
||||||
|
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||||
|
{
|
||||||
|
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||||
|
return ((string Signature, String Id, String Authorization))obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreData GetStoreData(this HttpContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
|
||||||
|
}
|
||||||
|
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
|
||||||
|
{
|
||||||
|
ctx.Items["BTCPAY.STOREDATA"] = storeData;
|
||||||
|
}
|
||||||
|
|
||||||
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||||
public static string ToJson(this object o)
|
public static string ToJson(this object o)
|
||||||
{
|
{
|
||||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HtmlString ToJSVariableModel(this object o, string variableName)
|
|
||||||
{
|
|
||||||
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
|
|
||||||
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,9 +43,7 @@ namespace BTCPayServer.Filters
|
||||||
|
|
||||||
public bool Accept(ActionConstraintContext context)
|
public bool Accept(ActionConstraintContext context)
|
||||||
{
|
{
|
||||||
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
|
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
|
||||||
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
|
|
||||||
return (hasVersion || hasIdentity) == IsBitpayAPI;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,13 @@ namespace BTCPayServer.HostedServices
|
||||||
{
|
{
|
||||||
get { return _creativeStartUri; }
|
get { return _creativeStartUri; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShowRegister { get; set; }
|
||||||
|
|
||||||
|
internal void Update(PoliciesSettings data)
|
||||||
|
{
|
||||||
|
ShowRegister = !data.LockSubscription;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CssThemeManagerHostedService : BaseAsyncService
|
public class CssThemeManagerHostedService : BaseAsyncService
|
||||||
|
@ -58,10 +65,19 @@ namespace BTCPayServer.HostedServices
|
||||||
{
|
{
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
CreateLoopTask(ListenForThemeChanges)
|
CreateLoopTask(ListenForThemeChanges),
|
||||||
|
CreateLoopTask(ListenForPoliciesChanges),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task ListenForPoliciesChanges()
|
||||||
|
{
|
||||||
|
await new SynchronizationContextRemover();
|
||||||
|
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||||
|
_CssThemeManager.Update(data);
|
||||||
|
await _SettingsRepository.WaitSettingsChanged<PoliciesSettings>(Cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
async Task ListenForThemeChanges()
|
async Task ListenForThemeChanges()
|
||||||
{
|
{
|
||||||
await new SynchronizationContextRemover();
|
await new SynchronizationContextRemover();
|
||||||
|
|
|
@ -198,7 +198,11 @@ namespace BTCPayServer.HostedServices
|
||||||
PosData = dto.PosData,
|
PosData = dto.PosData,
|
||||||
Price = dto.Price,
|
Price = dto.Price,
|
||||||
Status = dto.Status,
|
Status = dto.Status,
|
||||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
|
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
|
||||||
|
PaymentSubtotals = dto.PaymentSubtotals,
|
||||||
|
PaymentTotals = dto.PaymentTotals,
|
||||||
|
AmountPaid = dto.AmountPaid,
|
||||||
|
ExchangeRates = dto.ExchangeRates
|
||||||
};
|
};
|
||||||
|
|
||||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||||
|
@ -207,7 +211,7 @@ namespace BTCPayServer.HostedServices
|
||||||
if (btcCryptoInfo != null)
|
if (btcCryptoInfo != null)
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
notification.Rate = (double)dto.Rate;
|
notification.Rate = dto.Rate;
|
||||||
notification.Url = dto.Url;
|
notification.Url = dto.Url;
|
||||||
notification.BTCDue = dto.BTCDue;
|
notification.BTCDue = dto.BTCDue;
|
||||||
notification.BTCPaid = dto.BTCPaid;
|
notification.BTCPaid = dto.BTCPaid;
|
||||||
|
@ -305,7 +309,10 @@ namespace BTCPayServer.HostedServices
|
||||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||||
{
|
{
|
||||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||||
await SaveEvent(invoice.Id, e);
|
List<Task> tasks = new List<Task>();
|
||||||
|
|
||||||
|
// Awaiting this later help make sure invoices should arrive in order
|
||||||
|
tasks.Add(SaveEvent(invoice.Id, e));
|
||||||
|
|
||||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||||
if (invoice.FullNotifications)
|
if (invoice.FullNotifications)
|
||||||
|
@ -315,20 +322,22 @@ namespace BTCPayServer.HostedServices
|
||||||
e.Name == "invoice_failedToConfirm" ||
|
e.Name == "invoice_failedToConfirm" ||
|
||||||
e.Name == "invoice_markedInvalid" ||
|
e.Name == "invoice_markedInvalid" ||
|
||||||
e.Name == "invoice_failedToConfirm" ||
|
e.Name == "invoice_failedToConfirm" ||
|
||||||
e.Name == "invoice_completed"
|
e.Name == "invoice_completed" ||
|
||||||
|
e.Name == "invoice_expiredPaidPartial"
|
||||||
)
|
)
|
||||||
await Notify(invoice);
|
tasks.Add(Notify(invoice));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.Name == "invoice_confirmed")
|
if (e.Name == "invoice_confirmed")
|
||||||
{
|
{
|
||||||
await Notify(invoice);
|
tasks.Add(Notify(invoice));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invoice.ExtendedNotifications)
|
if (invoice.ExtendedNotifications)
|
||||||
{
|
{
|
||||||
await Notify(invoice, e.EventCode, e.Name);
|
tasks.Add(Notify(invoice, e.EventCode, e.Name));
|
||||||
}
|
}
|
||||||
|
await Task.WhenAll(tasks.ToArray());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,8 @@ namespace BTCPayServer.HostedServices
|
||||||
|
|
||||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||||
invoice.Status = "expired";
|
invoice.Status = "expired";
|
||||||
|
if(invoice.ExceptionStatus == "paidPartial")
|
||||||
|
context.Events.Add(new InvoiceEvent(invoice, 2000, "invoice_expiredPaidPartial"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||||
|
@ -78,7 +80,7 @@ namespace BTCPayServer.HostedServices
|
||||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||||
{
|
{
|
||||||
if (accounting.Paid >= accounting.TotalDue)
|
if (accounting.Paid >= accounting.MinimumTotalDue)
|
||||||
{
|
{
|
||||||
if (invoice.Status == "new")
|
if (invoice.Status == "new")
|
||||||
{
|
{
|
||||||
|
@ -96,17 +98,17 @@ namespace BTCPayServer.HostedServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||||
{
|
{
|
||||||
invoice.ExceptionStatus = "paidPartial";
|
invoice.ExceptionStatus = "paidPartial";
|
||||||
context.MarkDirty();
|
context.MarkDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just make sure RBF did not cancelled a payment
|
// Just make sure RBF did not cancelled a payment
|
||||||
if (invoice.Status == "paid")
|
if (invoice.Status == "paid")
|
||||||
{
|
{
|
||||||
if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||||
{
|
{
|
||||||
invoice.ExceptionStatus = null;
|
invoice.ExceptionStatus = null;
|
||||||
context.MarkDirty();
|
context.MarkDirty();
|
||||||
|
@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices
|
||||||
context.MarkDirty();
|
context.MarkDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounting.Paid < accounting.TotalDue)
|
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||||
{
|
{
|
||||||
invoice.Status = "new";
|
invoice.Status = "new";
|
||||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||||
|
@ -134,14 +136,14 @@ namespace BTCPayServer.HostedServices
|
||||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||||
&&
|
&&
|
||||||
// And not enough amount confirmed
|
// And not enough amount confirmed
|
||||||
(confirmedAccounting.Paid < accounting.TotalDue))
|
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||||
{
|
{
|
||||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||||
invoice.Status = "invalid";
|
invoice.Status = "invalid";
|
||||||
context.MarkDirty();
|
context.MarkDirty();
|
||||||
}
|
}
|
||||||
else if (confirmedAccounting.Paid >= accounting.TotalDue)
|
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||||
{
|
{
|
||||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||||
|
@ -153,7 +155,7 @@ namespace BTCPayServer.HostedServices
|
||||||
if (invoice.Status == "confirmed")
|
if (invoice.Status == "confirmed")
|
||||||
{
|
{
|
||||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||||
if (completedAccounting.Paid >= accounting.TotalDue)
|
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||||
{
|
{
|
||||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||||
invoice.Status = "complete";
|
invoice.Status = "complete";
|
||||||
|
@ -289,7 +291,7 @@ namespace BTCPayServer.HostedServices
|
||||||
if (updateContext.Dirty)
|
if (updateContext.Dirty)
|
||||||
{
|
{
|
||||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||||
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
|
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var evt in updateContext.Events)
|
foreach (var evt in updateContext.Events)
|
||||||
|
|
|
@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices
|
||||||
{
|
{
|
||||||
State = NBXplorerState.NotConnected;
|
State = NBXplorerState.NotConnected;
|
||||||
status = null;
|
status = null;
|
||||||
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
|
Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`");
|
||||||
}
|
}
|
||||||
|
|
||||||
_Dashboard.Publish(_Network, State, status, error);
|
_Dashboard.Publish(_Network, State, status, error);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ using NBitcoin;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
@ -38,55 +37,13 @@ using Microsoft.Extensions.Caching.Memory;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
|
||||||
namespace BTCPayServer.Hosting
|
namespace BTCPayServer.Hosting
|
||||||
{
|
{
|
||||||
public static class BTCPayServerServices
|
public static class BTCPayServerServices
|
||||||
{
|
{
|
||||||
public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
public OwnStoreAuthorizationRequirement()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public OwnStoreAuthorizationRequirement(string role)
|
|
||||||
{
|
|
||||||
Role = role;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Role
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OwnStoreHandler : AuthorizationHandler<OwnStoreAuthorizationRequirement>
|
|
||||||
{
|
|
||||||
StoreRepository _StoreRepository;
|
|
||||||
UserManager<ApplicationUser> _UserManager;
|
|
||||||
public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
|
||||||
{
|
|
||||||
_StoreRepository = storeRepository;
|
|
||||||
_UserManager = userManager;
|
|
||||||
}
|
|
||||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement)
|
|
||||||
{
|
|
||||||
object storeId = null;
|
|
||||||
if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
|
|
||||||
context.Succeed(requirement);
|
|
||||||
else if (storeId != null)
|
|
||||||
{
|
|
||||||
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
var store = await _StoreRepository.FindStore((string)storeId, user);
|
|
||||||
if (store != null)
|
|
||||||
if (requirement.Role == null || requirement.Role == store.Role)
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
|
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||||
|
@ -110,7 +67,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>();
|
||||||
|
@ -160,6 +116,8 @@ namespace BTCPayServer.Hosting
|
||||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||||
|
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||||
|
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
|
||||||
|
|
||||||
services.TryAddSingleton<ExplorerClientProvider>();
|
services.TryAddSingleton<ExplorerClientProvider>();
|
||||||
services.TryAddSingleton<Bitpay>(o =>
|
services.TryAddSingleton<Bitpay>(o =>
|
||||||
|
@ -169,30 +127,17 @@ 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.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
|
||||||
services.AddTransient<AccessTokenController>();
|
services.AddTransient<AccessTokenController>();
|
||||||
services.AddTransient<InvoiceController>();
|
services.AddTransient<InvoiceController>();
|
||||||
// Add application services.
|
// Add application services.
|
||||||
services.AddTransient<IEmailSender, EmailSender>();
|
services.AddTransient<IEmailSender, EmailSender>();
|
||||||
|
|
||||||
services.AddAuthorization(o =>
|
|
||||||
{
|
|
||||||
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
|
|
||||||
{
|
|
||||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
|
||||||
});
|
|
||||||
|
|
||||||
o.AddPolicy(StorePolicies.OwnStore, builder =>
|
|
||||||
{
|
|
||||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// bundling
|
// bundling
|
||||||
|
|
||||||
|
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||||
|
|
||||||
services.AddBundles();
|
services.AddBundles();
|
||||||
services.AddTransient<BundleOptions>(provider =>
|
services.AddTransient<BundleOptions>(provider =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,41 +6,25 @@ using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NBitcoin;
|
|
||||||
using NBitcoin.Crypto;
|
|
||||||
using NBitcoin.DataEncoders;
|
|
||||||
using Microsoft.AspNetCore.Http.Internal;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using BTCPayServer.Authentication;
|
using BTCPayServer.Authentication;
|
||||||
using System.Security.Principal;
|
|
||||||
using NBitpayClient.Extensions;
|
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using BTCPayServer.Controllers;
|
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Security.Claims;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services;
|
|
||||||
using NBitpayClient;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Hosting
|
namespace BTCPayServer.Hosting
|
||||||
{
|
{
|
||||||
public class BTCPayMiddleware
|
public class BTCPayMiddleware
|
||||||
{
|
{
|
||||||
TokenRepository _TokenRepository;
|
|
||||||
RequestDelegate _Next;
|
RequestDelegate _Next;
|
||||||
BTCPayServerOptions _Options;
|
BTCPayServerOptions _Options;
|
||||||
|
|
||||||
public BTCPayMiddleware(RequestDelegate next,
|
public BTCPayMiddleware(RequestDelegate next,
|
||||||
TokenRepository tokenRepo,
|
|
||||||
BTCPayServerOptions options)
|
BTCPayServerOptions options)
|
||||||
{
|
{
|
||||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
|
||||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
@ -49,15 +33,15 @@ namespace BTCPayServer.Hosting
|
||||||
public async Task Invoke(HttpContext httpContext)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
RewriteHostIfNeeded(httpContext);
|
RewriteHostIfNeeded(httpContext);
|
||||||
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
|
||||||
var sig = values.FirstOrDefault();
|
|
||||||
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
|
||||||
var id = values.FirstOrDefault();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
|
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||||
|
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
|
||||||
|
httpContext.SetIsBitpayAPI(isBitpayAPI);
|
||||||
|
if (isBitpayAPI)
|
||||||
{
|
{
|
||||||
await HandleBitId(httpContext, sig, id);
|
httpContext.SetBitpayAuth(bitpayAuth);
|
||||||
}
|
}
|
||||||
await _Next(httpContext);
|
await _Next(httpContext);
|
||||||
}
|
}
|
||||||
|
@ -76,7 +60,57 @@ namespace BTCPayServer.Hosting
|
||||||
Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware");
|
Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth)
|
||||||
|
{
|
||||||
|
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
||||||
|
var sig = values.FirstOrDefault();
|
||||||
|
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
||||||
|
var id = values.FirstOrDefault();
|
||||||
|
httpContext.Request.Headers.TryGetValue("Authorization", out values);
|
||||||
|
var auth = values.FirstOrDefault();
|
||||||
|
hasBitpayAuth = auth != null || (sig != null && id != null);
|
||||||
|
return (sig, id, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
|
||||||
|
{
|
||||||
|
if (!httpContext.Request.Path.HasValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var path = httpContext.Request.Path.Value;
|
||||||
|
if (
|
||||||
|
bitpayAuth &&
|
||||||
|
path == "/invoices" &&
|
||||||
|
httpContext.Request.Method == "POST" &&
|
||||||
|
isJson)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
bitpayAuth &&
|
||||||
|
path == "/invoices" &&
|
||||||
|
httpContext.Request.Method == "GET")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
httpContext.Request.Method == "GET" &&
|
||||||
|
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
httpContext.Request.Method == "GET")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||||
|
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
|
@ -170,90 +204,5 @@ namespace BTCPayServer.Hosting
|
||||||
await writer.FlushAsync();
|
await writer.FlushAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task HandleBitId(HttpContext httpContext, string sig, string id)
|
|
||||||
{
|
|
||||||
httpContext.Request.EnableRewind();
|
|
||||||
|
|
||||||
string body = string.Empty;
|
|
||||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
|
||||||
{
|
|
||||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
|
||||||
{
|
|
||||||
body = reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
httpContext.Request.Body.Position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = httpContext.Request.GetEncodedUrl();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var key = new PubKey(id);
|
|
||||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
|
||||||
{
|
|
||||||
var sin = key.GetBitIDSIN();
|
|
||||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
|
||||||
identity.AddClaim(new Claim(Claims.SIN, sin));
|
|
||||||
|
|
||||||
string token = null;
|
|
||||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
|
||||||
{
|
|
||||||
token = tokenValues[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token != null)
|
|
||||||
{
|
|
||||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
|
||||||
if (bitToken == null)
|
|
||||||
{
|
|
||||||
throw new BitpayHttpException(401, $"This endpoint does not support this facade");
|
|
||||||
}
|
|
||||||
identity.AddClaim(new Claim(Claims.OwnStore, bitToken.StoreId));
|
|
||||||
}
|
|
||||||
Logs.PayServer.LogDebug($"BitId signature check success for SIN {sin}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FormatException) { }
|
|
||||||
if (!httpContext.User.HasClaim(c => c.Type == Claims.SIN))
|
|
||||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
|
||||||
{
|
|
||||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
|
||||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
|
||||||
|
|
||||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
|
||||||
if (expectedToken == null || actualToken == null)
|
|
||||||
{
|
|
||||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return actualToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
|
||||||
{
|
|
||||||
if (token.Facade == Facade.Merchant.ToString())
|
|
||||||
{
|
|
||||||
yield return token.Clone(Facade.User);
|
|
||||||
yield return token.Clone(Facade.PointOfSale);
|
|
||||||
}
|
|
||||||
if (token.Facade == Facade.PointOfSale.ToString())
|
|
||||||
{
|
|
||||||
yield return token.Clone(Facade.User);
|
|
||||||
}
|
|
||||||
yield return token;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ using Hangfire.Annotations;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
@ -104,10 +103,6 @@ namespace BTCPayServer.Hosting
|
||||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
|
|
||||||
{
|
|
||||||
o.Value.DeveloperMode = _Env.IsDevelopment();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Needed to debug U2F for ledger support
|
// Needed to debug U2F for ledger support
|
||||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||||
|
@ -146,12 +141,8 @@ namespace BTCPayServer.Hosting
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
app.UseBrowserLink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//App insight do not that by itself...
|
|
||||||
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
|
|
||||||
|
|
||||||
app.UsePayServer();
|
app.UsePayServer();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using System;
|
||||||
using Microsoft.Extensions.Logging.Console;
|
|
||||||
using Microsoft.Extensions.Logging.Console.Internal;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions.Internal;
|
||||||
|
using Microsoft.Extensions.Logging.Console;
|
||||||
|
using Microsoft.Extensions.Logging.Console.Internal;
|
||||||
|
|
||||||
namespace BTCPayServer.Logging
|
namespace BTCPayServer.Logging
|
||||||
{
|
{
|
||||||
|
@ -20,19 +21,18 @@ namespace BTCPayServer.Logging
|
||||||
}
|
}
|
||||||
public ILogger CreateLogger(string categoryName)
|
public ILogger CreateLogger(string categoryName)
|
||||||
{
|
{
|
||||||
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
|
return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
|
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CustomConsoleLogger : ILogger
|
public class CustomerConsoleLogger : ILogger
|
||||||
{
|
{
|
||||||
private static readonly string _loglevelPadding = ": ";
|
private static readonly string _loglevelPadding = ": ";
|
||||||
private static readonly string _messagePadding;
|
private static readonly string _messagePadding;
|
||||||
|
@ -47,19 +47,33 @@ namespace BTCPayServer.Logging
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static StringBuilder _logBuilder;
|
private static StringBuilder _logBuilder;
|
||||||
|
|
||||||
static CustomConsoleLogger()
|
static CustomerConsoleLogger()
|
||||||
{
|
{
|
||||||
var logLevelString = GetLogLevelString(LogLevel.Information);
|
var logLevelString = GetLogLevelString(LogLevel.Information);
|
||||||
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
|
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
|
||||||
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
|
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
|
public CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
|
||||||
|
: this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor())
|
||||||
{
|
{
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
}
|
||||||
Filter = filter ?? ((category, logLevel) => true);
|
|
||||||
IncludeScopes = includeScopes;
|
|
||||||
|
|
||||||
|
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider)
|
||||||
|
: this(name, filter, scopeProvider, new ConsoleLoggerProcessor())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor)
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
Filter = filter ?? ((category, logLevel) => true);
|
||||||
|
ScopeProvider = scopeProvider;
|
||||||
_queueProcessor = loggerProcessor;
|
_queueProcessor = loggerProcessor;
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
@ -80,7 +94,12 @@ namespace BTCPayServer.Logging
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_queueProcessor.Console = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,13 +111,13 @@ namespace BTCPayServer.Logging
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_filter = value ?? throw new ArgumentNullException(nameof(value));
|
if (value == null)
|
||||||
}
|
{
|
||||||
}
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
public bool IncludeScopes
|
_filter = value;
|
||||||
{
|
}
|
||||||
get; set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
|
@ -106,6 +125,16 @@ namespace BTCPayServer.Logging
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal IExternalScopeProvider ScopeProvider
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisableColors
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
{
|
{
|
||||||
if (!IsEnabled(logLevel))
|
if (!IsEnabled(logLevel))
|
||||||
|
@ -154,10 +183,7 @@ namespace BTCPayServer.Logging
|
||||||
while (lenAfter++ < 18)
|
while (lenAfter++ < 18)
|
||||||
logBuilder.Append(" ");
|
logBuilder.Append(" ");
|
||||||
// scope information
|
// scope information
|
||||||
if (IncludeScopes)
|
GetScopeInformation(logBuilder);
|
||||||
{
|
|
||||||
GetScopeInformation(logBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(message))
|
if (!string.IsNullOrEmpty(message))
|
||||||
{
|
{
|
||||||
|
@ -202,18 +228,15 @@ namespace BTCPayServer.Logging
|
||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
{
|
{
|
||||||
|
if (logLevel == LogLevel.None)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return Filter(Name, logLevel);
|
return Filter(Name, logLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable BeginScope<TState>(TState state)
|
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;
|
||||||
{
|
|
||||||
if (state == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConsoleLogScope.Push(Name, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLogLevelString(LogLevel logLevel)
|
private static string GetLogLevelString(LogLevel logLevel)
|
||||||
{
|
{
|
||||||
|
@ -238,6 +261,11 @@ namespace BTCPayServer.Logging
|
||||||
|
|
||||||
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
|
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
|
||||||
{
|
{
|
||||||
|
if (DisableColors)
|
||||||
|
{
|
||||||
|
return new ConsoleColors(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
// We must explicitly set the background color if we are setting the foreground color,
|
// We must explicitly set the background color if we are setting the foreground color,
|
||||||
// since just setting one can look bad on the users console.
|
// since just setting one can look bad on the users console.
|
||||||
switch (logLevel)
|
switch (logLevel)
|
||||||
|
@ -259,30 +287,25 @@ namespace BTCPayServer.Logging
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GetScopeInformation(StringBuilder builder)
|
private void GetScopeInformation(StringBuilder stringBuilder)
|
||||||
{
|
{
|
||||||
var current = ConsoleLogScope.Current;
|
var scopeProvider = ScopeProvider;
|
||||||
string scopeLog = string.Empty;
|
if (scopeProvider != null)
|
||||||
var length = builder.Length;
|
|
||||||
|
|
||||||
while (current != null)
|
|
||||||
{
|
{
|
||||||
if (length == builder.Length)
|
var initialLength = stringBuilder.Length;
|
||||||
{
|
|
||||||
scopeLog = $"=> {current}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
scopeLog = $"=> {current} ";
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Insert(length, scopeLog);
|
scopeProvider.ForEachScope((scope, state) =>
|
||||||
current = current.Parent;
|
{
|
||||||
}
|
var (builder, length) = state;
|
||||||
if (builder.Length > length)
|
var first = length == builder.Length;
|
||||||
{
|
builder.Append(first ? "=> " : " => ").Append(scope);
|
||||||
builder.Insert(length, _messagePadding);
|
}, (stringBuilder, initialLength));
|
||||||
builder.AppendLine();
|
|
||||||
|
if (stringBuilder.Length > initialLength)
|
||||||
|
{
|
||||||
|
stringBuilder.Insert(initialLength, _messagePadding);
|
||||||
|
stringBuilder.AppendLine();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,9 +356,9 @@ namespace BTCPayServer.Logging
|
||||||
// Start Console message queue processor
|
// Start Console message queue processor
|
||||||
_outputTask = Task.Factory.StartNew(
|
_outputTask = Task.Factory.StartNew(
|
||||||
ProcessLogQueue,
|
ProcessLogQueue,
|
||||||
this,
|
state: this,
|
||||||
default(CancellationToken),
|
cancellationToken: default(CancellationToken),
|
||||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void EnqueueMessage(LogMessageEntry message)
|
public virtual void EnqueueMessage(LogMessageEntry message)
|
||||||
|
|
|
@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetRoles",
|
name: "AspNetRoles",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
Name = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetUsers",
|
name: "AspNetUsers",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
|
AccessFailedCount = table.Column<int>(nullable: false),
|
||||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||||
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
Email = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
EmailConfirmed = table.Column<bool>(nullable: false),
|
EmailConfirmed = table.Column<bool>(nullable: false),
|
||||||
LockoutEnabled = table.Column<bool>(nullable: false),
|
LockoutEnabled = table.Column<bool>(nullable: false),
|
||||||
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
||||||
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
|
PasswordHash = table.Column<string>(nullable: true),
|
||||||
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
|
PhoneNumber = table.Column<string>(nullable: true),
|
||||||
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
||||||
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
|
SecurityStamp = table.Column<string>(nullable: true),
|
||||||
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
||||||
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
UserName = table.Column<string>(maxLength: 256, nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations
|
||||||
name: "Stores",
|
name: "Stores",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
|
DerivationStrategy = table.Column<string>(nullable: true),
|
||||||
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
|
SpeedPolicy = table.Column<int>(nullable: false),
|
||||||
StoreCertificate = table.Column<byte[]>(nullable: true),
|
StoreCertificate = table.Column<byte[]>(nullable: true),
|
||||||
StoreName = table.Column<string>(type: "TEXT", nullable: true),
|
StoreName = table.Column<string>(nullable: true),
|
||||||
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
|
StoreWebsite = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetRoleClaims",
|
name: "AspNetRoleClaims",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
Id = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
ClaimType = table.Column<string>(nullable: true),
|
||||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
ClaimValue = table.Column<string>(nullable: true),
|
||||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
RoleId = table.Column<string>(nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetUserClaims",
|
name: "AspNetUserClaims",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
Id = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
ClaimType = table.Column<string>(nullable: true),
|
||||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
ClaimValue = table.Column<string>(nullable: true),
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
UserId = table.Column<string>(nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetUserLogins",
|
name: "AspNetUserLogins",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
LoginProvider = table.Column<string>(nullable: false),
|
||||||
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
|
ProviderKey = table.Column<string>(nullable: false),
|
||||||
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
ProviderDisplayName = table.Column<string>(nullable: true),
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
UserId = table.Column<string>(nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetUserRoles",
|
name: "AspNetUserRoles",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
UserId = table.Column<string>(nullable: false),
|
||||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
RoleId = table.Column<string>(nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AspNetUserTokens",
|
name: "AspNetUserTokens",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
UserId = table.Column<string>(nullable: false),
|
||||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
LoginProvider = table.Column<string>(nullable: false),
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
Name = table.Column<string>(nullable: false),
|
||||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
Value = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations
|
||||||
name: "Invoices",
|
name: "Invoices",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
Blob = table.Column<byte[]>(nullable: true),
|
Blob = table.Column<byte[]>(nullable: true),
|
||||||
Created = table.Column<DateTimeOffset>(nullable: false),
|
Created = table.Column<DateTimeOffset>(nullable: false),
|
||||||
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
|
CustomerEmail = table.Column<string>(nullable: true),
|
||||||
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
|
ExceptionStatus = table.Column<string>(nullable: true),
|
||||||
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
|
ItemCode = table.Column<string>(nullable: true),
|
||||||
OrderId = table.Column<string>(type: "TEXT", nullable: true),
|
OrderId = table.Column<string>(nullable: true),
|
||||||
Status = table.Column<string>(type: "TEXT", nullable: true),
|
Status = table.Column<string>(nullable: true),
|
||||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
StoreDataId = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations
|
||||||
name: "UserStore",
|
name: "UserStore",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
ApplicationUserId = table.Column<string>(nullable: false),
|
||||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
|
StoreDataId = table.Column<string>(nullable: false),
|
||||||
Role = table.Column<string>(type: "TEXT", nullable: true)
|
Role = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations
|
||||||
name: "Payments",
|
name: "Payments",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
Blob = table.Column<byte[]>(nullable: true),
|
Blob = table.Column<byte[]>(nullable: true),
|
||||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
InvoiceDataId = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations
|
||||||
name: "RefundAddresses",
|
name: "RefundAddresses",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
Blob = table.Column<byte[]>(nullable: true),
|
Blob = table.Column<byte[]>(nullable: true),
|
||||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
InvoiceDataId = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
Value = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
|
||||||
name: "AddressInvoices",
|
name: "AddressInvoices",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
Address = table.Column<string>(nullable: false),
|
||||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
InvoiceDataId = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations
|
||||||
name: "PairedSINData",
|
name: "PairedSINData",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
Facade = table.Column<string>(type: "TEXT", nullable: true),
|
Facade = table.Column<string>(nullable: true),
|
||||||
Label = table.Column<string>(type: "TEXT", nullable: true),
|
Label = table.Column<string>(nullable: true),
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
Name = table.Column<string>(nullable: true),
|
||||||
PairingTime = table.Column<DateTimeOffset>(nullable: false),
|
PairingTime = table.Column<DateTimeOffset>(nullable: false),
|
||||||
SIN = table.Column<string>(type: "TEXT", nullable: true),
|
SIN = table.Column<string>(nullable: true),
|
||||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
StoreDataId = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations
|
||||||
name: "PairingCodes",
|
name: "PairingCodes",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
DateCreated = table.Column<DateTime>(nullable: false),
|
DateCreated = table.Column<DateTime>(nullable: false),
|
||||||
Expiration = table.Column<DateTimeOffset>(nullable: false),
|
Expiration = table.Column<DateTimeOffset>(nullable: false),
|
||||||
Facade = table.Column<string>(type: "TEXT", nullable: true),
|
Facade = table.Column<string>(nullable: true),
|
||||||
Label = table.Column<string>(type: "TEXT", nullable: true),
|
Label = table.Column<string>(nullable: true),
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
Name = table.Column<string>(nullable: true),
|
||||||
SIN = table.Column<string>(type: "TEXT", nullable: true),
|
SIN = table.Column<string>(nullable: true),
|
||||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
StoreDataId = table.Column<string>(nullable: true),
|
||||||
TokenValue = table.Column<string>(type: "TEXT", nullable: true)
|
TokenValue = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
|
||||||
name: "PendingInvoices",
|
name: "PendingInvoices",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false)
|
Id = table.Column<string>(nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations
|
||||||
name: "HistoricalAddressInvoices",
|
name: "HistoricalAddressInvoices",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: false),
|
InvoiceDataId = table.Column<string>(nullable: false),
|
||||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
Address = table.Column<string>(nullable: false),
|
||||||
Assigned = table.Column<DateTimeOffset>(nullable: false),
|
Assigned = table.Column<DateTimeOffset>(nullable: false),
|
||||||
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
|
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
|
||||||
},
|
},
|
||||||
|
|
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
|
@ -0,0 +1,553 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20180429083930_legacyapikey")]
|
||||||
|
partial class legacyapikey
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CreatedTime");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Address");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("AddressInvoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("AppType");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Settings");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("Apps");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.Property<string>("Address");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Assigned");
|
||||||
|
|
||||||
|
b.Property<string>("CryptoCode");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("UnAssigned");
|
||||||
|
|
||||||
|
b.HasKey("InvoiceDataId", "Address");
|
||||||
|
|
||||||
|
b.ToTable("HistoricalAddressInvoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerEmail");
|
||||||
|
|
||||||
|
b.Property<string>("ExceptionStatus");
|
||||||
|
|
||||||
|
b.Property<string>("ItemCode");
|
||||||
|
|
||||||
|
b.Property<string>("OrderId");
|
||||||
|
|
||||||
|
b.Property<string>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("Invoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.Property<string>("UniqueId");
|
||||||
|
|
||||||
|
b.Property<string>("Message");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Timestamp");
|
||||||
|
|
||||||
|
b.HasKey("InvoiceDataId", "UniqueId");
|
||||||
|
|
||||||
|
b.ToTable("InvoiceEvents");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Facade");
|
||||||
|
|
||||||
|
b.Property<string>("Label");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("PairingTime");
|
||||||
|
|
||||||
|
b.Property<string>("SIN");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SIN");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("PairedSINData");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Expiration");
|
||||||
|
|
||||||
|
b.Property<string>("Facade");
|
||||||
|
|
||||||
|
b.Property<string>("Label");
|
||||||
|
|
||||||
|
b.Property<string>("SIN");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.Property<string>("TokenValue");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PairingCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<bool>("Accounted");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("Payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PendingInvoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("RefundAddresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCrypto");
|
||||||
|
|
||||||
|
b.Property<string>("DerivationStrategies");
|
||||||
|
|
||||||
|
b.Property<string>("DerivationStrategy");
|
||||||
|
|
||||||
|
b.Property<int>("SpeedPolicy");
|
||||||
|
|
||||||
|
b.Property<byte[]>("StoreBlob");
|
||||||
|
|
||||||
|
b.Property<byte[]>("StoreCertificate");
|
||||||
|
|
||||||
|
b.Property<string>("StoreName");
|
||||||
|
|
||||||
|
b.Property<string>("StoreWebsite");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Stores");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ApplicationUserId");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.Property<string>("Role");
|
||||||
|
|
||||||
|
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("UserStore");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("RequiresEmailConfirmation");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("AddressInvoices")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("Apps")
|
||||||
|
.HasForeignKey("StoreDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||||
|
.WithMany("HistoricalAddressInvoices")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StoreDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||||
|
.WithMany("Events")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("Payments")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("RefundAddresses")
|
||||||
|
.HasForeignKey("InvoiceDataId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("ApplicationUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
public partial class legacyapikey : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiKeys",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(maxLength: 50, nullable: false),
|
||||||
|
StoreId = table.Column<string>(maxLength: 50, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiKeys", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiKeys_StoreId",
|
||||||
|
table: "ApiKeys",
|
||||||
|
column: "StoreId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiKeys");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||||
{
|
{
|
||||||
|
@ -36,6 +36,22 @@ namespace BTCPayServer.Migrations
|
||||||
b.ToTable("AddressInvoices");
|
b.ToTable("AddressInvoices");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
|
|
@ -19,5 +19,6 @@ namespace BTCPayServer.Models
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
public string ButtonClass { get; set; } = "btn-danger";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Models
|
|
||||||
{
|
|
||||||
public class ErrorViewModel
|
|
||||||
{
|
|
||||||
public string RequestId { get; set; }
|
|
||||||
|
|
||||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -79,7 +79,7 @@ namespace BTCPayServer.Models
|
||||||
|
|
||||||
//"price":5
|
//"price":5
|
||||||
[JsonProperty("price")]
|
[JsonProperty("price")]
|
||||||
public double Price
|
public decimal Price
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ namespace BTCPayServer.Models
|
||||||
//"exRates":{"USD":4320.02}
|
//"exRates":{"USD":4320.02}
|
||||||
[JsonProperty("exRates")]
|
[JsonProperty("exRates")]
|
||||||
[Obsolete("Use CryptoInfo.ExRates instead")]
|
[Obsolete("Use CryptoInfo.ExRates instead")]
|
||||||
public Dictionary<string, double> ExRates
|
public Dictionary<string, decimal> ExRates
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
@ -224,6 +224,29 @@ namespace BTCPayServer.Models
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonProperty("paymentSubtotals")]
|
||||||
|
public Dictionary<string, long> PaymentSubtotals { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("paymentTotals")]
|
||||||
|
public Dictionary<string, long> PaymentTotals { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("amountPaid")]
|
||||||
|
public long AmountPaid { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("minerFees")]
|
||||||
|
public long MinerFees { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("exchangeRates")]
|
||||||
|
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("supportedTransactionCurrencies")]
|
||||||
|
public Dictionary<string, NBitpayClient.InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("addresses")]
|
||||||
|
public Dictionary<string, string> Addresses { get; set; }
|
||||||
|
[JsonProperty("paymentCodes")]
|
||||||
|
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes{get; set;}
|
||||||
}
|
}
|
||||||
public class Flags
|
public class Flags
|
||||||
{
|
{
|
||||||
|
@ -233,4 +256,5 @@ namespace BTCPayServer.Models
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.InvoicingModels
|
namespace BTCPayServer.Models.InvoicingModels
|
||||||
{
|
{
|
||||||
|
@ -14,7 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
Currency = "USD";
|
Currency = "USD";
|
||||||
}
|
}
|
||||||
[Required]
|
[Required]
|
||||||
public double? Amount
|
public decimal? Amount
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
@ -52,8 +53,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Uri]
|
||||||
[Url]
|
|
||||||
public string NotificationUrl
|
public string NotificationUrl
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
|
|
@ -18,6 +18,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
public string Address { get; internal set; }
|
public string Address { get; internal set; }
|
||||||
public string Rate { get; internal set; }
|
public string Rate { get; internal set; }
|
||||||
public string PaymentUrl { get; internal set; }
|
public string PaymentUrl { get; internal set; }
|
||||||
|
public string Overpaid { get; set; }
|
||||||
}
|
}
|
||||||
public class AddressModel
|
public class AddressModel
|
||||||
{
|
{
|
||||||
|
|
|
@ -49,6 +49,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
public bool ShowCheckout { get; set; }
|
||||||
|
public string ExceptionStatus { get; set; }
|
||||||
public string AmountCurrency
|
public string AmountCurrency
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
|
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
public string CryptoImage { get; set; }
|
public string CryptoImage { get; set; }
|
||||||
public string Link { get; set; }
|
public string Link { get; set; }
|
||||||
}
|
}
|
||||||
|
public string HtmlTitle { get; set; }
|
||||||
public string CustomCSSLink { get; set; }
|
public string CustomCSSLink { get; set; }
|
||||||
public string CustomLogoLink { get; set; }
|
public string CustomLogoLink { get; set; }
|
||||||
public string DefaultLang { get; set; }
|
public string DefaultLang { get; set; }
|
||||||
|
@ -36,6 +37,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
public string TimeLeft { get; set; }
|
public string TimeLeft { get; set; }
|
||||||
public string Rate { get; set; }
|
public string Rate { get; set; }
|
||||||
public string OrderAmount { get; set; }
|
public string OrderAmount { get; set; }
|
||||||
|
public string OrderAmountFiat { get; set; }
|
||||||
public string InvoiceBitcoinUrl { get; set; }
|
public string InvoiceBitcoinUrl { get; set; }
|
||||||
public string InvoiceBitcoinUrlQR { get; set; }
|
public string InvoiceBitcoinUrlQR { get; set; }
|
||||||
public int TxCount { get; set; }
|
public int TxCount { get; set; }
|
||||||
|
|
|
@ -18,8 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string TestEmail
|
public string TestEmail
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
@ -42,12 +43,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
public string OnChainMinValue { get; set; }
|
public string OnChainMinValue { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Link to a custom CSS stylesheet")]
|
[Display(Name = "Link to a custom CSS stylesheet")]
|
||||||
[Url]
|
[Uri]
|
||||||
public string CustomCSS { get; set; }
|
public string CustomCSS { get; set; }
|
||||||
[Display(Name = "Link to a custom logo")]
|
[Display(Name = "Link to a custom logo")]
|
||||||
[Url]
|
[Uri]
|
||||||
public string CustomLogo { get; set; }
|
public string CustomLogo { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Custom HTML title to display on Checkout page")]
|
||||||
|
public string HtmlTitle { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
||||||
{
|
{
|
||||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||||
|
|
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
{
|
||||||
|
public class RatesViewModel
|
||||||
|
{
|
||||||
|
public class TestResultViewModel
|
||||||
|
{
|
||||||
|
public string CurrencyPair { get; set; }
|
||||||
|
public string Rule { get; set; }
|
||||||
|
public bool Error { get; set; }
|
||||||
|
}
|
||||||
|
class Format
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||||
|
{
|
||||||
|
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||||
|
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||||
|
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||||
|
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||||
|
PreferredExchange = chosen.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||||
|
|
||||||
|
public SelectList Exchanges { get; set; }
|
||||||
|
|
||||||
|
public bool ShowScripting { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Rate rules")]
|
||||||
|
[MaxLength(2000)]
|
||||||
|
public string Script { get; set; }
|
||||||
|
public string DefaultScript { get; set; }
|
||||||
|
public string ScriptTest { get; set; }
|
||||||
|
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Multiply the rate by ...")]
|
||||||
|
[Range(0.01, 10.0)]
|
||||||
|
public double RateMultiplier
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||||
|
public string PreferredExchange { get; set; }
|
||||||
|
|
||||||
|
public string RateSource
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
using BTCPayServer.Validations;
|
using BTCPayServer.Validations;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using System;
|
using System;
|
||||||
|
@ -12,11 +14,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
{
|
{
|
||||||
public class StoreViewModel
|
public class StoreViewModel
|
||||||
{
|
{
|
||||||
class Format
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Value { get; set; }
|
|
||||||
}
|
|
||||||
public class DerivationScheme
|
public class DerivationScheme
|
||||||
{
|
{
|
||||||
public string Crypto { get; set; }
|
public string Crypto { get; set; }
|
||||||
|
@ -38,7 +35,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Url]
|
[Uri]
|
||||||
[Display(Name = "Store Website")]
|
[Display(Name = "Store Website")]
|
||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
public string StoreWebsite
|
public string StoreWebsite
|
||||||
|
@ -49,36 +46,6 @@ 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)
|
|
||||||
{
|
|
||||||
var defaultStore = preferredExchange ?? "coinaverage";
|
|
||||||
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
|
|
||||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
|
||||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
|
||||||
PreferredExchange = chosen.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SelectList Exchanges { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
|
||||||
public string PreferredExchange { get; set; }
|
|
||||||
|
|
||||||
public string RateSource
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Display(Name = "Multiply the original rate by ...")]
|
|
||||||
[Range(0.01, 10.0)]
|
|
||||||
public double RateMultiplier
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||||
[Range(1, 60 * 24 * 24)]
|
[Range(1, 60 * 24 * 24)]
|
||||||
public int InvoiceExpiration
|
public int InvoiceExpiration
|
||||||
|
@ -119,5 +86,13 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = new List<LightningNode>();
|
} = new List<LightningNode>();
|
||||||
|
|
||||||
|
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
|
||||||
|
[Range(0, 100)]
|
||||||
|
public double PaymentTolerance
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Display(Name = "API Key")]
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
public string EncodedApiKey { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
{
|
{
|
||||||
return ConfirmationCount >= 1;
|
return ConfirmationCount >= 1;
|
||||||
}
|
}
|
||||||
|
else if (speedPolicy == SpeedPolicy.LowMediumSpeed)
|
||||||
|
{
|
||||||
|
return ConfirmationCount >= 2;
|
||||||
|
}
|
||||||
else if (speedPolicy == SpeedPolicy.LowSpeed)
|
else if (speedPolicy == SpeedPolicy.LowSpeed)
|
||||||
{
|
{
|
||||||
return ConfirmationCount >= 6;
|
return ConfirmationCount >= 6;
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
{
|
{
|
||||||
EventAggregator _Aggregator;
|
EventAggregator _Aggregator;
|
||||||
ExplorerClientProvider _ExplorerClients;
|
ExplorerClientProvider _ExplorerClients;
|
||||||
IApplicationLifetime _Lifetime;
|
Microsoft.Extensions.Hosting.IApplicationLifetime _Lifetime;
|
||||||
InvoiceRepository _InvoiceRepository;
|
InvoiceRepository _InvoiceRepository;
|
||||||
private TaskCompletionSource<bool> _RunningTask;
|
private TaskCompletionSource<bool> _RunningTask;
|
||||||
private CancellationTokenSource _Cts;
|
private CancellationTokenSource _Cts;
|
||||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
BTCPayWalletProvider wallets,
|
BTCPayWalletProvider wallets,
|
||||||
InvoiceRepository invoiceRepository,
|
InvoiceRepository invoiceRepository,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
EventAggregator aggregator, IApplicationLifetime lifetime)
|
EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime)
|
||||||
{
|
{
|
||||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||||
_Wallets = wallets;
|
_Wallets = wallets;
|
||||||
|
|
|
@ -156,7 +156,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
|
||||||
|
|
||||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
|
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
|
||||||
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
|
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,17 +36,25 @@ namespace BTCPayServer.Payments.Lightning
|
||||||
expiry = TimeSpan.FromSeconds(1);
|
expiry = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
LightningInvoice lightningInvoice = null;
|
LightningInvoice lightningInvoice = null;
|
||||||
try
|
|
||||||
|
string description = storeBlob.LightningDescriptionTemplate;
|
||||||
|
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
using (var cts = new CancellationTokenSource(5000))
|
||||||
{
|
{
|
||||||
string description = storeBlob.LightningDescriptionTemplate;
|
try
|
||||||
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
{
|
||||||
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry, cts.Token);
|
||||||
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
}
|
||||||
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
|
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
||||||
{
|
}
|
||||||
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var nodeInfo = await test;
|
var nodeInfo = await test;
|
||||||
return new LightningLikePaymentMethodDetails()
|
return new LightningLikePaymentMethodDetails()
|
||||||
|
@ -62,34 +70,36 @@ namespace BTCPayServer.Payments.Lightning
|
||||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||||
|
|
||||||
var cts = new CancellationTokenSource(5000);
|
using (var cts = new CancellationTokenSource(5000))
|
||||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
|
||||||
LightningNodeInformation info = null;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
info = await client.GetInfo(cts.Token);
|
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||||
}
|
LightningNodeInformation info = null;
|
||||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
try
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
info = await client.GetInfo(cts.Token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||||
|
}
|
||||||
|
|
||||||
if (info.Address == null)
|
if (info.Address == null)
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||||
if (blocksGap > 10)
|
if (blocksGap > 10)
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
|
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
|
||||||
|
|
|
@ -40,7 +40,6 @@ namespace BTCPayServer
|
||||||
.UseIISIntegration()
|
.UseIISIntegration()
|
||||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
.UseConfiguration(conf)
|
.UseConfiguration(conf)
|
||||||
.UseApplicationInsights()
|
|
||||||
.ConfigureLogging(l =>
|
.ConfigureLogging(l =>
|
||||||
{
|
{
|
||||||
l.AddFilter("Microsoft", LogLevel.Error);
|
l.AddFilter("Microsoft", LogLevel.Error);
|
||||||
|
|
104
BTCPayServer/Rating/CurrencyPair.cs
Normal file
104
BTCPayServer/Rating/CurrencyPair.cs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Rating
|
||||||
|
{
|
||||||
|
public class CurrencyPair
|
||||||
|
{
|
||||||
|
static readonly BTCPayNetworkProvider _NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
|
||||||
|
public CurrencyPair(string left, string right)
|
||||||
|
{
|
||||||
|
if (right == null)
|
||||||
|
throw new ArgumentNullException(nameof(right));
|
||||||
|
if (left == null)
|
||||||
|
throw new ArgumentNullException(nameof(left));
|
||||||
|
Right = right.ToUpperInvariant();
|
||||||
|
Left = left.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
public string Left { get; private set; }
|
||||||
|
public string Right { get; private set; }
|
||||||
|
|
||||||
|
public static CurrencyPair Parse(string str)
|
||||||
|
{
|
||||||
|
if (!TryParse(str, out var result))
|
||||||
|
throw new FormatException("Invalid currency pair");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public static bool TryParse(string str, out CurrencyPair value)
|
||||||
|
{
|
||||||
|
if (str == null)
|
||||||
|
throw new ArgumentNullException(nameof(str));
|
||||||
|
value = null;
|
||||||
|
str = str.Trim();
|
||||||
|
if (str.Length > 12)
|
||||||
|
return false;
|
||||||
|
var splitted = str.Split(new[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (splitted.Length == 2)
|
||||||
|
{
|
||||||
|
value = new CurrencyPair(splitted[0], splitted[1]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (splitted.Length == 1)
|
||||||
|
{
|
||||||
|
var currencyPair = splitted[0];
|
||||||
|
if (currencyPair.Length < 6 || currencyPair.Length > 10)
|
||||||
|
return false;
|
||||||
|
if (currencyPair.Length == 6)
|
||||||
|
{
|
||||||
|
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 3; i < 5; i++)
|
||||||
|
{
|
||||||
|
var potentialCryptoName = currencyPair.Substring(0, i);
|
||||||
|
var network = _NetworkProvider.GetNetwork(potentialCryptoName);
|
||||||
|
if (network != null)
|
||||||
|
{
|
||||||
|
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
CurrencyPair item = obj as CurrencyPair;
|
||||||
|
if (item == null)
|
||||||
|
return false;
|
||||||
|
return ToString().Equals(item.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
public static bool operator ==(CurrencyPair a, CurrencyPair b)
|
||||||
|
{
|
||||||
|
if (System.Object.ReferenceEquals(a, b))
|
||||||
|
return true;
|
||||||
|
if (((object)a == null) || ((object)b == null))
|
||||||
|
return false;
|
||||||
|
return a.ToString() == b.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(CurrencyPair a, CurrencyPair b)
|
||||||
|
{
|
||||||
|
return !(a == b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return ToString().GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Left}_{Right}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrencyPair Inverse()
|
||||||
|
{
|
||||||
|
return new CurrencyPair(Right, Left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
BTCPayServer/Rating/ExchangeRates.cs
Normal file
96
BTCPayServer/Rating/ExchangeRates.cs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Rating
|
||||||
|
{
|
||||||
|
public class ExchangeRates : IEnumerable<ExchangeRate>
|
||||||
|
{
|
||||||
|
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
|
||||||
|
public ExchangeRates()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public ExchangeRates(IEnumerable<ExchangeRate> rates)
|
||||||
|
{
|
||||||
|
foreach (var rate in rates)
|
||||||
|
{
|
||||||
|
Add(rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<ExchangeRate> _Rates = new List<ExchangeRate>();
|
||||||
|
public MultiValueDictionary<string, ExchangeRate> ByExchange
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
} = new MultiValueDictionary<string, ExchangeRate>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
ByExchange.Add(rate.Exchange, rate);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (rate.Value.HasValue)
|
||||||
|
{
|
||||||
|
_AllRates[key].Value = rate.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<ExchangeRate> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _Rates.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
|
||||||
|
{
|
||||||
|
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||||
|
{
|
||||||
|
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||||
|
if (rate != null)
|
||||||
|
rate.Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
|
||||||
|
{
|
||||||
|
if (currencyPair.Left == currencyPair.Right)
|
||||||
|
return 1.0m;
|
||||||
|
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||||
|
{
|
||||||
|
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||||
|
if (rate != null)
|
||||||
|
return rate.Value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class ExchangeRate
|
||||||
|
{
|
||||||
|
public string Exchange { get; set; }
|
||||||
|
public CurrencyPair CurrencyPair { get; set; }
|
||||||
|
public decimal? Value { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
if (Value == null)
|
||||||
|
return $"{Exchange}({CurrencyPair})";
|
||||||
|
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
538
BTCPayServer/Rating/RateRules.cs
Normal file
538
BTCPayServer/Rating/RateRules.cs
Normal file
|
@ -0,0 +1,538 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Rating
|
||||||
|
{
|
||||||
|
public enum RateRulesErrors
|
||||||
|
{
|
||||||
|
Ok,
|
||||||
|
TooMuchNestedCalls,
|
||||||
|
InvalidCurrencyIdentifier,
|
||||||
|
NestedInvocation,
|
||||||
|
UnsupportedOperator,
|
||||||
|
MissingArgument,
|
||||||
|
DivideByZero,
|
||||||
|
PreprocessError,
|
||||||
|
RateUnavailable,
|
||||||
|
InvalidExchangeName,
|
||||||
|
}
|
||||||
|
public class RateRules
|
||||||
|
{
|
||||||
|
class NormalizeCurrencyPairsRewritter : CSharpSyntaxRewriter
|
||||||
|
{
|
||||||
|
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||||
|
|
||||||
|
bool IsInvocation;
|
||||||
|
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||||
|
{
|
||||||
|
if (IsInvocation)
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.NestedInvocation);
|
||||||
|
return base.VisitInvocationExpression(node);
|
||||||
|
}
|
||||||
|
if (node.Expression is IdentifierNameSyntax id)
|
||||||
|
{
|
||||||
|
IsInvocation = true;
|
||||||
|
var arglist = (ArgumentListSyntax)this.Visit(node.ArgumentList);
|
||||||
|
IsInvocation = false;
|
||||||
|
return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(id.Identifier.ValueText.ToLowerInvariant()), arglist)
|
||||||
|
.WithTriviaFrom(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.InvalidExchangeName);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||||
|
{
|
||||||
|
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currencyPair))
|
||||||
|
{
|
||||||
|
return SyntaxFactory.IdentifierName(currencyPair.ToString())
|
||||||
|
.WithTriviaFrom(node);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier);
|
||||||
|
return base.VisitIdentifierName(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class RuleList : CSharpSyntaxWalker
|
||||||
|
{
|
||||||
|
public Dictionary<CurrencyPair, (ExpressionSyntax Expression, SyntaxNode Trivia)> ExpressionsByPair = new Dictionary<CurrencyPair, (ExpressionSyntax Expression, SyntaxNode Trivia)>();
|
||||||
|
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
|
||||||
|
{
|
||||||
|
if (node.Kind() == SyntaxKind.SimpleAssignmentExpression
|
||||||
|
&& node.Left is IdentifierNameSyntax id
|
||||||
|
&& node.Right is ExpressionSyntax expression)
|
||||||
|
{
|
||||||
|
if (CurrencyPair.TryParse(id.Identifier.ValueText, out var currencyPair))
|
||||||
|
{
|
||||||
|
expression = expression.WithTriviaFrom(expression);
|
||||||
|
ExpressionsByPair.Add(currencyPair, (expression, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.VisitAssignmentExpression(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyntaxNode GetSyntaxNode()
|
||||||
|
{
|
||||||
|
return SyntaxFactory.Block(
|
||||||
|
ExpressionsByPair.Select(e =>
|
||||||
|
SyntaxFactory.ExpressionStatement(
|
||||||
|
SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
|
||||||
|
SyntaxFactory.IdentifierName(e.Key.ToString()).WithTriviaFrom(e.Value.Trivia),
|
||||||
|
e.Value.Expression)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SyntaxNode root;
|
||||||
|
RuleList ruleList;
|
||||||
|
|
||||||
|
public decimal GlobalMultiplier { get; set; } = 1.0m;
|
||||||
|
|
||||||
|
RateRules(SyntaxNode root)
|
||||||
|
{
|
||||||
|
ruleList = new RuleList();
|
||||||
|
ruleList.Visit(root);
|
||||||
|
// Remove every irrelevant statements
|
||||||
|
this.root = ruleList.GetSyntaxNode();
|
||||||
|
}
|
||||||
|
public static bool TryParse(string str, out RateRules rules)
|
||||||
|
{
|
||||||
|
return TryParse(str, out rules, out var unused);
|
||||||
|
}
|
||||||
|
public static bool TryParse(string str, out RateRules rules, out List<RateRulesErrors> errors)
|
||||||
|
{
|
||||||
|
rules = null;
|
||||||
|
errors = null;
|
||||||
|
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
|
||||||
|
var rewriter = new NormalizeCurrencyPairsRewritter();
|
||||||
|
// Rename BTC_usd to BTC_USD and verify structure
|
||||||
|
var root = rewriter.Visit(expression.GetRoot());
|
||||||
|
if (rewriter.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
errors = rewriter.Errors;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rules = new RateRules(root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateRule GetRuleFor(CurrencyPair currencyPair)
|
||||||
|
{
|
||||||
|
if (currencyPair.Left == "X" || currencyPair.Right == "X")
|
||||||
|
throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency");
|
||||||
|
var candidate = FindBestCandidate(currencyPair);
|
||||||
|
if (GlobalMultiplier != decimal.One)
|
||||||
|
{
|
||||||
|
candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
return new RateRule(this, currencyPair, candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpressionSyntax FindBestCandidate(CurrencyPair p)
|
||||||
|
{
|
||||||
|
var invP = p.Inverse();
|
||||||
|
var candidates = new List<(CurrencyPair Pair, int Prioriy, ExpressionSyntax Expression, bool Inverse)>();
|
||||||
|
foreach (var pair in new[]
|
||||||
|
{
|
||||||
|
(Pair: p, Priority: 0, Inverse: false),
|
||||||
|
(Pair: new CurrencyPair(p.Left, "X"), Priority: 1, Inverse: false),
|
||||||
|
(Pair: new CurrencyPair("X", p.Right), Priority: 1, Inverse: false),
|
||||||
|
(Pair: invP, Priority: 2, Inverse: true),
|
||||||
|
(Pair: new CurrencyPair(invP.Left, "X"), Priority: 3, Inverse: true),
|
||||||
|
(Pair: new CurrencyPair("X", invP.Right), Priority: 3, Inverse: true),
|
||||||
|
(Pair: new CurrencyPair("X", "X"), Priority: 4, Inverse: false)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (ruleList.ExpressionsByPair.TryGetValue(pair.Pair, out var expression))
|
||||||
|
{
|
||||||
|
candidates.Add((pair.Pair, pair.Priority, expression.Expression, pair.Inverse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
return CreateExpression($"ERR_NO_RULE_MATCH({p})");
|
||||||
|
var best = candidates
|
||||||
|
.OrderBy(c => c.Prioriy)
|
||||||
|
.ThenBy(c => c.Expression.Span.Start)
|
||||||
|
.First();
|
||||||
|
return best.Inverse
|
||||||
|
? CreateExpression($"1 / {invP}")
|
||||||
|
: best.Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ExpressionSyntax CreateExpression(string str)
|
||||||
|
{
|
||||||
|
return (ExpressionSyntax)CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script)).GetRoot().ChildNodes().First().ChildNodes().First().ChildNodes().First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return root.NormalizeWhitespace("", "\n")
|
||||||
|
.ToFullString()
|
||||||
|
.Replace("{\n", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("\n}", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RateRule
|
||||||
|
{
|
||||||
|
class ReplaceExchangeRateRewriter : CSharpSyntaxRewriter
|
||||||
|
{
|
||||||
|
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||||
|
public ExchangeRates Rates;
|
||||||
|
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||||
|
{
|
||||||
|
var exchangeName = node.Expression.ToString();
|
||||||
|
if (exchangeName.StartsWith("ERR_", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.PreprocessError);
|
||||||
|
return base.VisitInvocationExpression(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currencyPair = node.ArgumentList.ChildNodes().FirstOrDefault()?.ToString();
|
||||||
|
if (currencyPair == null || !CurrencyPair.TryParse(currencyPair, out var pair))
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier);
|
||||||
|
return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var rate = Rates.GetRate(exchangeName, pair);
|
||||||
|
if (rate == null)
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.RateUnavailable);
|
||||||
|
return RateRules.CreateExpression($"ERR_RATE_UNAVAILABLE({exchangeName}, {pair.ToString()})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var token = SyntaxFactory.ParseToken(rate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
|
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CalculateWalker : CSharpSyntaxWalker
|
||||||
|
{
|
||||||
|
public Stack<decimal> Values = new Stack<decimal>();
|
||||||
|
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||||
|
|
||||||
|
public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node)
|
||||||
|
{
|
||||||
|
base.VisitPrefixUnaryExpression(node);
|
||||||
|
bool invalid = false;
|
||||||
|
switch (node.Kind())
|
||||||
|
{
|
||||||
|
case SyntaxKind.UnaryMinusExpression:
|
||||||
|
case SyntaxKind.UnaryPlusExpression:
|
||||||
|
if (Values.Count < 1)
|
||||||
|
{
|
||||||
|
invalid = true;
|
||||||
|
Errors.Add(RateRulesErrors.MissingArgument);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
invalid = true;
|
||||||
|
Errors.Add(RateRulesErrors.UnsupportedOperator);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (node.Kind())
|
||||||
|
{
|
||||||
|
case SyntaxKind.UnaryMinusExpression:
|
||||||
|
Values.Push(-Values.Pop());
|
||||||
|
break;
|
||||||
|
case SyntaxKind.UnaryPlusExpression:
|
||||||
|
Values.Push(+Values.Pop());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException("Should never happen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitBinaryExpression(BinaryExpressionSyntax node)
|
||||||
|
{
|
||||||
|
base.VisitBinaryExpression(node);
|
||||||
|
|
||||||
|
|
||||||
|
bool invalid = false;
|
||||||
|
switch (node.Kind())
|
||||||
|
{
|
||||||
|
case SyntaxKind.AddExpression:
|
||||||
|
case SyntaxKind.MultiplyExpression:
|
||||||
|
case SyntaxKind.DivideExpression:
|
||||||
|
case SyntaxKind.SubtractExpression:
|
||||||
|
if (Values.Count < 2)
|
||||||
|
{
|
||||||
|
invalid = true;
|
||||||
|
Errors.Add(RateRulesErrors.MissingArgument);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var b = Values.Pop();
|
||||||
|
var a = Values.Pop();
|
||||||
|
|
||||||
|
switch (node.Kind())
|
||||||
|
{
|
||||||
|
case SyntaxKind.AddExpression:
|
||||||
|
Values.Push(a + b);
|
||||||
|
break;
|
||||||
|
case SyntaxKind.MultiplyExpression:
|
||||||
|
Values.Push(a * b);
|
||||||
|
break;
|
||||||
|
case SyntaxKind.DivideExpression:
|
||||||
|
if (b == decimal.Zero)
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.DivideByZero);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Values.Push(a / b);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SyntaxKind.SubtractExpression:
|
||||||
|
Values.Push(a - b);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException("Should never happen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitLiteralExpression(LiteralExpressionSyntax node)
|
||||||
|
{
|
||||||
|
switch (node.Kind())
|
||||||
|
{
|
||||||
|
case SyntaxKind.NumericLiteralExpression:
|
||||||
|
Values.Push(decimal.Parse(node.ToString(), CultureInfo.InvariantCulture));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HasBinaryOperations : CSharpSyntaxWalker
|
||||||
|
{
|
||||||
|
public bool Result = false;
|
||||||
|
public override void VisitBinaryExpression(BinaryExpressionSyntax node)
|
||||||
|
{
|
||||||
|
base.VisitBinaryExpression(node);
|
||||||
|
switch (node.Kind())
|
||||||
|
{
|
||||||
|
case SyntaxKind.AddExpression:
|
||||||
|
case SyntaxKind.MultiplyExpression:
|
||||||
|
case SyntaxKind.DivideExpression:
|
||||||
|
case SyntaxKind.MinusToken:
|
||||||
|
Result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class FlattenExpressionRewriter : CSharpSyntaxRewriter
|
||||||
|
{
|
||||||
|
RateRules parent;
|
||||||
|
CurrencyPair pair;
|
||||||
|
int nested = 0;
|
||||||
|
public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair)
|
||||||
|
{
|
||||||
|
this.pair = pair;
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeRates ExchangeRates = new ExchangeRates();
|
||||||
|
bool IsInvocation;
|
||||||
|
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||||
|
{
|
||||||
|
if (IsInvocation)
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier);
|
||||||
|
return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})");
|
||||||
|
}
|
||||||
|
IsInvocation = true;
|
||||||
|
_ExchangeName = node.Expression.ToString();
|
||||||
|
var result = base.VisitInvocationExpression(node);
|
||||||
|
IsInvocation = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsArgumentList;
|
||||||
|
public override SyntaxNode VisitArgumentList(ArgumentListSyntax node)
|
||||||
|
{
|
||||||
|
IsArgumentList = true;
|
||||||
|
var result = base.VisitArgumentList(node);
|
||||||
|
IsArgumentList = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
string _ExchangeName = null;
|
||||||
|
|
||||||
|
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||||
|
const int MaxNestedCount = 8;
|
||||||
|
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
(!IsInvocation || IsArgumentList) &&
|
||||||
|
CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
|
||||||
|
{
|
||||||
|
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? pair.Left : currentPair.Left,
|
||||||
|
right: currentPair.Right == "X" ? pair.Right : currentPair.Right);
|
||||||
|
if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD)
|
||||||
|
{
|
||||||
|
ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName });
|
||||||
|
return SyntaxFactory.IdentifierName(replacedPair.ToString());
|
||||||
|
}
|
||||||
|
else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD
|
||||||
|
{
|
||||||
|
var bestCandidate = parent.FindBestCandidate(replacedPair);
|
||||||
|
if (nested > MaxNestedCount)
|
||||||
|
{
|
||||||
|
Errors.Add(RateRulesErrors.TooMuchNestedCalls);
|
||||||
|
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||||
|
}
|
||||||
|
var innerFlatten = CreateNewContext(replacedPair);
|
||||||
|
var replaced = innerFlatten.Visit(bestCandidate);
|
||||||
|
if (replaced is ExpressionSyntax expression)
|
||||||
|
{
|
||||||
|
var hasBinaryOps = new HasBinaryOperations();
|
||||||
|
hasBinaryOps.Visit(expression);
|
||||||
|
if (hasBinaryOps.Result)
|
||||||
|
{
|
||||||
|
replaced = SyntaxFactory.ParenthesizedExpression(expression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls))
|
||||||
|
{
|
||||||
|
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||||
|
}
|
||||||
|
return replaced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base.VisitIdentifierName(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlattenExpressionRewriter CreateNewContext(CurrencyPair pair)
|
||||||
|
{
|
||||||
|
return new FlattenExpressionRewriter(parent, pair)
|
||||||
|
{
|
||||||
|
Errors = Errors,
|
||||||
|
nested = nested + 1,
|
||||||
|
ExchangeRates = ExchangeRates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private SyntaxNode expression;
|
||||||
|
FlattenExpressionRewriter flatten;
|
||||||
|
|
||||||
|
public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate)
|
||||||
|
{
|
||||||
|
_CurrencyPair = currencyPair;
|
||||||
|
flatten = new FlattenExpressionRewriter(parent, currencyPair);
|
||||||
|
this.expression = flatten.Visit(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly CurrencyPair _CurrencyPair;
|
||||||
|
public CurrencyPair CurrencyPair
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _CurrencyPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeRates ExchangeRates
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return flatten.ExchangeRates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool Reevaluate()
|
||||||
|
{
|
||||||
|
_Value = null;
|
||||||
|
_EvaluatedNode = null;
|
||||||
|
_Evaluated = null;
|
||||||
|
Errors.Clear();
|
||||||
|
|
||||||
|
var rewriter = new ReplaceExchangeRateRewriter();
|
||||||
|
rewriter.Rates = ExchangeRates;
|
||||||
|
var result = rewriter.Visit(this.expression);
|
||||||
|
Errors.AddRange(rewriter.Errors);
|
||||||
|
_Evaluated = result.NormalizeWhitespace("", "\n").ToString();
|
||||||
|
if (HasError)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var calculate = new CalculateWalker();
|
||||||
|
calculate.Visit(result);
|
||||||
|
if (calculate.Values.Count != 1 || calculate.Errors.Count != 0)
|
||||||
|
{
|
||||||
|
Errors.AddRange(calculate.Errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_Value = calculate.Values.Pop();
|
||||||
|
_EvaluatedNode = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly HashSet<RateRulesErrors> _Errors = new HashSet<RateRulesErrors>();
|
||||||
|
public HashSet<RateRulesErrors> Errors
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _Errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SyntaxNode _EvaluatedNode;
|
||||||
|
string _Evaluated;
|
||||||
|
public bool HasError
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _Errors.Count != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToString(bool evaluated)
|
||||||
|
{
|
||||||
|
if (!evaluated)
|
||||||
|
return ToString();
|
||||||
|
if (_Evaluated == null)
|
||||||
|
return "Call Evaluate() first";
|
||||||
|
return _Evaluated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return expression.NormalizeWhitespace("", "\n").ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal? _Value;
|
||||||
|
public decimal? Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
BTCPayServer/Security/BTCPayClaimsFilter.cs
Normal file
66
BTCPayServer/Security/BTCPayClaimsFilter.cs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Security
|
||||||
|
{
|
||||||
|
|
||||||
|
public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||||
|
{
|
||||||
|
UserManager<ApplicationUser> _UserManager;
|
||||||
|
StoreRepository _StoreRepository;
|
||||||
|
public BTCPayClaimsFilter(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_UserManager = userManager;
|
||||||
|
_StoreRepository = storeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||||
|
{
|
||||||
|
options.Filters.Add(typeof(BTCPayClaimsFilter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
var principal = context.HttpContext.User;
|
||||||
|
if (!context.HttpContext.GetIsBitpayAPI())
|
||||||
|
{
|
||||||
|
var identity = ((ClaimsIdentity)principal.Identity);
|
||||||
|
if (principal.IsInRole(Roles.ServerAdmin))
|
||||||
|
{
|
||||||
|
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
|
||||||
|
}
|
||||||
|
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
|
||||||
|
{
|
||||||
|
var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
|
||||||
|
if (claim != null)
|
||||||
|
{
|
||||||
|
var store = await _StoreRepository.FindStore((string)storeId, claim.Value);
|
||||||
|
if (store == null)
|
||||||
|
context.Result = new ChallengeResult(Policies.CookieAuthentication);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.HttpContext.SetStoreData(store);
|
||||||
|
if (store != null)
|
||||||
|
{
|
||||||
|
identity.AddClaims(store.GetClaims());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
196
BTCPayServer/Security/BitpayClaimsFilter.cs
Normal file
196
BTCPayServer/Security/BitpayClaimsFilter.cs
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Authentication;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
|
using NBitpayClient;
|
||||||
|
using NBitpayClient.Extensions;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using BTCPayServer.Logging;
|
||||||
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Security
|
||||||
|
{
|
||||||
|
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||||
|
{
|
||||||
|
UserManager<ApplicationUser> _UserManager;
|
||||||
|
StoreRepository _StoreRepository;
|
||||||
|
TokenRepository _TokenRepository;
|
||||||
|
|
||||||
|
public BitpayClaimsFilter(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
TokenRepository tokenRepository,
|
||||||
|
StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_UserManager = userManager;
|
||||||
|
_StoreRepository = storeRepository;
|
||||||
|
_TokenRepository = tokenRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||||
|
{
|
||||||
|
options.Filters.Add(typeof(BitpayClaimsFilter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
var principal = context.HttpContext.User;
|
||||||
|
if (context.HttpContext.GetIsBitpayAPI())
|
||||||
|
{
|
||||||
|
var bitpayAuth = context.HttpContext.GetBitpayAuth();
|
||||||
|
string storeId = null;
|
||||||
|
var failedAuth = false;
|
||||||
|
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||||
|
{
|
||||||
|
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||||
|
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
|
||||||
|
{
|
||||||
|
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||||
|
failedAuth = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||||
|
{
|
||||||
|
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
|
||||||
|
if (storeId == null)
|
||||||
|
{
|
||||||
|
Logs.PayServer.LogDebug("API key check failed");
|
||||||
|
failedAuth = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeId != null)
|
||||||
|
{
|
||||||
|
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
|
||||||
|
identity.AddClaim(new Claim(Policies.CanUseStore.Key, storeId));
|
||||||
|
var store = await _StoreRepository.FindStore(storeId);
|
||||||
|
context.HttpContext.SetStoreData(store);
|
||||||
|
}
|
||||||
|
else if (failedAuth)
|
||||||
|
{
|
||||||
|
throw new BitpayHttpException(401, "Invalid credentials");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
|
||||||
|
{
|
||||||
|
httpContext.Request.EnableRewind();
|
||||||
|
|
||||||
|
string storeId = null;
|
||||||
|
string body = string.Empty;
|
||||||
|
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||||
|
{
|
||||||
|
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||||
|
{
|
||||||
|
body = reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
httpContext.Request.Body.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = httpContext.Request.GetEncodedUrl();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = new PubKey(id);
|
||||||
|
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||||
|
{
|
||||||
|
var sin = key.GetBitIDSIN();
|
||||||
|
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||||
|
identity.AddClaim(new Claim(Claims.SIN, sin));
|
||||||
|
|
||||||
|
string token = null;
|
||||||
|
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||||
|
{
|
||||||
|
token = tokenValues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token != null)
|
||||||
|
{
|
||||||
|
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||||
|
if (bitToken == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
storeId = bitToken.StoreId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FormatException) { }
|
||||||
|
return storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||||
|
{
|
||||||
|
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string apiKey = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||||
|
{
|
||||||
|
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||||
|
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||||
|
|
||||||
|
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||||
|
if (expectedToken == null || actualToken == null)
|
||||||
|
{
|
||||||
|
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actualToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||||
|
{
|
||||||
|
if (token.Facade == Facade.Merchant.ToString())
|
||||||
|
{
|
||||||
|
yield return token.Clone(Facade.User);
|
||||||
|
yield return token.Clone(Facade.PointOfSale);
|
||||||
|
}
|
||||||
|
if (token.Facade == Facade.PointOfSale.ToString())
|
||||||
|
{
|
||||||
|
yield return token.Clone(Facade.User);
|
||||||
|
}
|
||||||
|
yield return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
BTCPayServer/Security/Policies.cs
Normal file
38
BTCPayServer/Security/Policies.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Security
|
||||||
|
{
|
||||||
|
public static class Policies
|
||||||
|
{
|
||||||
|
public const string CookieAuthentication = "Identity.Application";
|
||||||
|
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||||
|
{
|
||||||
|
AddClaim(options, CanUseStore.Key);
|
||||||
|
AddClaim(options, CanModifyStoreSettings.Key);
|
||||||
|
AddClaim(options, CanModifyServerSettings.Key);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddClaim(AuthorizationOptions options, string key)
|
||||||
|
{
|
||||||
|
options.AddPolicy(key, o => o.RequireClaim(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CanModifyServerSettings
|
||||||
|
{
|
||||||
|
public const string Key = "btcpay.store.canmodifyserversettings";
|
||||||
|
}
|
||||||
|
public class CanUseStore
|
||||||
|
{
|
||||||
|
public const string Key = "btcpay.store.canusestore";
|
||||||
|
}
|
||||||
|
public class CanModifyStoreSettings
|
||||||
|
{
|
||||||
|
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,8 @@ namespace BTCPayServer.Services.Fees
|
||||||
ExplorerClient _ExplorerClient;
|
ExplorerClient _ExplorerClient;
|
||||||
public async Task<FeeRate> GetFeeRateAsync()
|
public async Task<FeeRate> GetFeeRateAsync()
|
||||||
{
|
{
|
||||||
|
if (!_ExplorerClient.Network.SupportEstimatesSmartFee)
|
||||||
|
return _Factory.Fallback;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
|
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
|
||||||
|
|
|
@ -118,18 +118,7 @@ namespace BTCPayServer.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
|
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||||
{
|
|
||||||
if (network == null)
|
|
||||||
throw new ArgumentNullException(nameof(network));
|
|
||||||
if (strategy == null)
|
|
||||||
throw new ArgumentNullException(nameof(strategy));
|
|
||||||
if (!strategy.Segwit)
|
|
||||||
return false;
|
|
||||||
return await GetKeyPath(_Ledger, network, strategy) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
|
||||||
{
|
{
|
||||||
List<KeyPath> derivations = new List<KeyPath>();
|
List<KeyPath> derivations = new List<KeyPath>();
|
||||||
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
|
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||||
|
@ -143,7 +132,7 @@ namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var extpubkey = await GetExtPubKey(ledger, network, account, true);
|
var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
|
||||||
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
||||||
{
|
{
|
||||||
foundKeyPath = account;
|
foundKeyPath = account;
|
||||||
|
@ -159,79 +148,12 @@ namespace BTCPayServer.Services
|
||||||
return foundKeyPath;
|
return foundKeyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
|
public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
|
||||||
ReceivedCoin[] coins, BTCPayNetwork network,
|
Transaction unsigned,
|
||||||
(IDestination destination, Money amount, bool substractFees)[] send,
|
KeyPath changeKeyPath)
|
||||||
FeeRate feeRate,
|
|
||||||
IDestination changeAddress,
|
|
||||||
KeyPath changeKeyPath,
|
|
||||||
FeeRate minTxRelayFee)
|
|
||||||
{
|
{
|
||||||
if (strategy == null)
|
|
||||||
throw new ArgumentNullException(nameof(strategy));
|
|
||||||
if (network == null)
|
|
||||||
throw new ArgumentNullException(nameof(network));
|
|
||||||
if (feeRate == null)
|
|
||||||
throw new ArgumentNullException(nameof(feeRate));
|
|
||||||
if (changeAddress == null)
|
|
||||||
throw new ArgumentNullException(nameof(changeAddress));
|
|
||||||
if (feeRate.FeePerK <= Money.Zero)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var element in send)
|
|
||||||
{
|
|
||||||
if (element.destination == null)
|
|
||||||
throw new ArgumentNullException(nameof(element.destination));
|
|
||||||
if (element.amount == null)
|
|
||||||
throw new ArgumentNullException(nameof(element.amount));
|
|
||||||
if (element.amount <= Money.Zero)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
|
|
||||||
|
|
||||||
if (foundKeyPath == null)
|
|
||||||
{
|
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
|
||||||
}
|
|
||||||
|
|
||||||
TransactionBuilder builder = new TransactionBuilder();
|
|
||||||
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
|
|
||||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
|
||||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
|
||||||
|
|
||||||
foreach (var element in send)
|
|
||||||
{
|
|
||||||
builder.Send(element.destination, element.amount);
|
|
||||||
if (element.substractFees)
|
|
||||||
builder.SubtractFees();
|
|
||||||
}
|
|
||||||
builder.SetChange(changeAddress);
|
|
||||||
builder.SendEstimatedFees(feeRate);
|
|
||||||
builder.Shuffle();
|
|
||||||
var unsigned = builder.BuildTransaction(false);
|
|
||||||
|
|
||||||
var keypaths = new Dictionary<Script, KeyPath>();
|
|
||||||
foreach(var c in coins)
|
|
||||||
{
|
|
||||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasChange = unsigned.Outputs.Count == 2;
|
|
||||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
|
||||||
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
||||||
var fullySigned = await Ledger.SignTransactionAsync(
|
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
|
||||||
usedCoins.Select(c => new SignatureRequest
|
|
||||||
{
|
|
||||||
InputCoin = c,
|
|
||||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
|
||||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
|
||||||
}).ToArray(),
|
|
||||||
unsigned,
|
|
||||||
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
|
|
||||||
return fullySigned;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ using NBXplorer.Models;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
|
using NBitpayClient;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Invoices
|
namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
|
@ -100,7 +101,8 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
HighSpeed = 0,
|
HighSpeed = 0,
|
||||||
MediumSpeed = 1,
|
MediumSpeed = 1,
|
||||||
LowSpeed = 2
|
LowSpeed = 2,
|
||||||
|
LowMediumSpeed = 3
|
||||||
}
|
}
|
||||||
public class InvoiceEntity
|
public class InvoiceEntity
|
||||||
{
|
{
|
||||||
|
@ -314,6 +316,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
}
|
}
|
||||||
public bool ExtendedNotifications { get; set; }
|
public bool ExtendedNotifications { get; set; }
|
||||||
public List<InvoiceEventData> Events { get; internal set; }
|
public List<InvoiceEventData> Events { get; internal set; }
|
||||||
|
public double PaymentTolerance { get; set; }
|
||||||
|
|
||||||
public bool IsExpired()
|
public bool IsExpired()
|
||||||
{
|
{
|
||||||
|
@ -334,18 +337,35 @@ namespace BTCPayServer.Services.Invoices
|
||||||
ExpirationTime = ExpirationTime,
|
ExpirationTime = ExpirationTime,
|
||||||
Status = Status,
|
Status = Status,
|
||||||
Currency = ProductInformation.Currency,
|
Currency = ProductInformation.Currency,
|
||||||
Flags = new Flags() { Refundable = Refundable }
|
Flags = new Flags() { Refundable = Refundable },
|
||||||
|
|
||||||
|
PaymentSubtotals = new Dictionary<string, long>(),
|
||||||
|
PaymentTotals= new Dictionary<string, long>(),
|
||||||
|
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>(),
|
||||||
|
Addresses = new Dictionary<string, string>(),
|
||||||
|
PaymentCodes = new Dictionary<string, InvoicePaymentUrls>(),
|
||||||
|
ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
|
||||||
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;
|
var subtotalPrice = accounting.TotalDue - accounting.NetworkFee;
|
||||||
|
var cryptoCode = info.GetId().CryptoCode;
|
||||||
|
var address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
|
||||||
|
var exrates = new Dictionary<string, decimal>
|
||||||
|
{
|
||||||
|
{ ProductInformation.Currency, cryptoInfo.Rate }
|
||||||
|
};
|
||||||
|
|
||||||
|
cryptoInfo.CryptoCode = 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 = subtotalPrice.ToString();
|
||||||
|
|
||||||
cryptoInfo.Due = accounting.Due.ToString();
|
cryptoInfo.Due = accounting.Due.ToString();
|
||||||
cryptoInfo.Paid = accounting.Paid.ToString();
|
cryptoInfo.Paid = accounting.Paid.ToString();
|
||||||
|
@ -354,19 +374,16 @@ namespace BTCPayServer.Services.Invoices
|
||||||
cryptoInfo.TxCount = accounting.TxCount;
|
cryptoInfo.TxCount = accounting.TxCount;
|
||||||
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
|
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
|
||||||
|
|
||||||
cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
|
cryptoInfo.Address = address;
|
||||||
cryptoInfo.ExRates = new Dictionary<string, double>
|
|
||||||
{
|
cryptoInfo.ExRates = exrates;
|
||||||
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
|
var paymentId = info.GetId();
|
||||||
};
|
|
||||||
|
|
||||||
var scheme = info.Network.UriScheme;
|
var scheme = info.Network.UriScheme;
|
||||||
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
|
||||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
|
|
||||||
|
|
||||||
|
if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||||
if (info.GetId().PaymentType == PaymentTypes.BTCLike)
|
|
||||||
{
|
{
|
||||||
|
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
||||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||||
{
|
{
|
||||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||||
|
@ -375,7 +392,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var paymentId = info.GetId();
|
|
||||||
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
||||||
{
|
{
|
||||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||||
|
@ -386,7 +403,6 @@ namespace BTCPayServer.Services.Invoices
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
|
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||||
{
|
{
|
||||||
dto.Url = cryptoInfo.Url;
|
|
||||||
dto.BTCPrice = cryptoInfo.Price;
|
dto.BTCPrice = cryptoInfo.Price;
|
||||||
dto.Rate = cryptoInfo.Rate;
|
dto.Rate = cryptoInfo.Rate;
|
||||||
dto.ExRates = cryptoInfo.ExRates;
|
dto.ExRates = cryptoInfo.ExRates;
|
||||||
|
@ -395,17 +411,28 @@ namespace BTCPayServer.Services.Invoices
|
||||||
dto.BTCDue = cryptoInfo.Due;
|
dto.BTCDue = cryptoInfo.Due;
|
||||||
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);
|
|
||||||
|
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
|
||||||
|
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
|
||||||
|
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
|
||||||
|
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
|
||||||
|
{
|
||||||
|
Enabled = true
|
||||||
|
});
|
||||||
|
dto.Addresses.Add(paymentId.ToString(), address);
|
||||||
|
dto.ExchangeRates.TryAdd(cryptoCode, exrates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice
|
||||||
|
|
||||||
Populate(ProductInformation, dto);
|
Populate(ProductInformation, dto);
|
||||||
Populate(BuyerInformation, dto);
|
Populate(BuyerInformation, dto);
|
||||||
|
|
||||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||||
dto.Guid = Guid.NewGuid().ToString();
|
dto.Guid = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
|
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
@ -432,26 +459,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;
|
||||||
|
@ -537,6 +553,10 @@ namespace BTCPayServer.Services.Invoices
|
||||||
/// Total amount of network fee to pay to the invoice
|
/// Total amount of network fee to pay to the invoice
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Money NetworkFee { get; set; }
|
public Money NetworkFee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum required to be paid in order to accept invocie as paid
|
||||||
|
/// </summary>
|
||||||
|
public Money MinimumTotalDue { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PaymentMethod
|
public class PaymentMethod
|
||||||
|
@ -635,20 +655,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 +679,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 +696,16 @@ 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;
|
||||||
|
accounting.MinimumTotalDue = Money.Max(Money.Satoshis(1), Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))));
|
||||||
return accounting;
|
return accounting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -762,7 +778,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
paymentData.Outpoint = Outpoint;
|
paymentData.Outpoint = Outpoint;
|
||||||
return paymentData;
|
return paymentData;
|
||||||
}
|
}
|
||||||
if(GetPaymentMethodId().PaymentType== PaymentTypes.LightningLike)
|
if (GetPaymentMethodId().PaymentType == PaymentTypes.LightningLike)
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
|
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
invoice.StoreId = storeId;
|
invoice.StoreId = storeId;
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
context.Invoices.Add(new InvoiceData()
|
context.Invoices.Add(new Data.InvoiceData()
|
||||||
{
|
{
|
||||||
StoreDataId = storeId,
|
StoreDataId = storeId,
|
||||||
Id = invoice.Id,
|
Id = invoice.Id,
|
||||||
|
@ -267,7 +267,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||||
if (invoiceData == null)
|
if (invoiceData == null)
|
||||||
return;
|
return;
|
||||||
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob, null);
|
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob, null);
|
||||||
|
@ -307,7 +307,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||||
if (invoiceData == null)
|
if (invoiceData == null)
|
||||||
return;
|
return;
|
||||||
invoiceData.Status = status;
|
invoiceData.Status = status;
|
||||||
|
@ -320,7 +320,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||||
if (invoiceData?.Status != "paid")
|
if (invoiceData?.Status != "paid")
|
||||||
return;
|
return;
|
||||||
invoiceData.Status = "invalid";
|
invoiceData.Status = "invalid";
|
||||||
|
@ -331,7 +331,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
IQueryable<InvoiceData> query =
|
IQueryable<Data.InvoiceData> query =
|
||||||
context
|
context
|
||||||
.Invoices
|
.Invoices
|
||||||
.Include(o => o.Payments)
|
.Include(o => o.Payments)
|
||||||
|
@ -351,7 +351,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvoiceEntity ToEntity(InvoiceData invoice)
|
private InvoiceEntity ToEntity(Data.InvoiceData invoice)
|
||||||
{
|
{
|
||||||
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
|
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
|
@ -386,7 +386,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
IQueryable<InvoiceData> query = context
|
IQueryable<Data.InvoiceData> query = context
|
||||||
.Invoices
|
.Invoices
|
||||||
.Include(o => o.Payments)
|
.Include(o => o.Payments)
|
||||||
.Include(o => o.RefundAddresses);
|
.Include(o => o.RefundAddresses);
|
||||||
|
@ -436,6 +436,18 @@ namespace BTCPayServer.Services.Invoices
|
||||||
query = query.Where(i => statusSet.Contains(i.Status));
|
query = query.Where(i => statusSet.Contains(i.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(queryObject.Unusual != null)
|
||||||
|
{
|
||||||
|
var unused = queryObject.Unusual.Value;
|
||||||
|
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
|
||||||
|
{
|
||||||
|
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
|
||||||
|
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
|
||||||
|
}
|
||||||
|
|
||||||
query = query.OrderByDescending(q => q.Created);
|
query = query.OrderByDescending(q => q.Created);
|
||||||
|
|
||||||
if (queryObject.Skip != null)
|
if (queryObject.Skip != null)
|
||||||
|
@ -451,6 +463,29 @@ namespace BTCPayServer.Services.Invoices
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string NormalizeExceptionStatus(string status)
|
||||||
|
{
|
||||||
|
status = status.ToLowerInvariant();
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case "paidover":
|
||||||
|
case "over":
|
||||||
|
case "overpaid":
|
||||||
|
status = "paidOver";
|
||||||
|
break;
|
||||||
|
case "paidlate":
|
||||||
|
case "late":
|
||||||
|
status = "paidLate";
|
||||||
|
break;
|
||||||
|
case "paidpartial":
|
||||||
|
case "underpaid":
|
||||||
|
case "partial":
|
||||||
|
status = "paidPartial";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network)
|
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network)
|
||||||
{
|
{
|
||||||
if (outputs.Length == 0)
|
if (outputs.Length == 0)
|
||||||
|
@ -614,10 +649,18 @@ namespace BTCPayServer.Services.Invoices
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool? Unusual { get; set; }
|
||||||
|
|
||||||
public string[] Status
|
public string[] Status
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] ExceptionStatus
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
public string InvoiceId
|
public string InvoiceId
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
|
|
|
@ -31,6 +31,7 @@ namespace BTCPayServer.Services
|
||||||
new Language("nl-NL", "Dutch"),
|
new Language("nl-NL", "Dutch"),
|
||||||
new Language("cs-CZ", "Česky"),
|
new Language("cs-CZ", "Česky"),
|
||||||
new Language("is-IS", "Íslenska"),
|
new Language("is-IS", "Íslenska"),
|
||||||
|
new Language("hr-HR", "Croatian"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,8 @@ namespace BTCPayServer.Services.Mails
|
||||||
}
|
}
|
||||||
public async Task SendEmailAsync(string email, string subject, string message)
|
public async Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||||
if (settings == null)
|
if (!settings.IsComplete())
|
||||||
{
|
{
|
||||||
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
||||||
return;
|
return;
|
||||||
|
@ -36,8 +36,8 @@ namespace BTCPayServer.Services.Mails
|
||||||
|
|
||||||
public async Task SendMailCore(string email, string subject, string message)
|
public async Task SendMailCore(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||||
if (settings == null)
|
if (!settings.IsComplete())
|
||||||
throw new InvalidOperationException("Email settings not configured");
|
throw new InvalidOperationException("Email settings not configured");
|
||||||
var smtp = settings.CreateSmtpClient();
|
var smtp = settings.CreateSmtpClient();
|
||||||
MailMessage mail = new MailMessage(settings.From, email, subject, message);
|
MailMessage mail = new MailMessage(settings.From, email, subject, message);
|
||||||
|
|
|
@ -10,30 +10,25 @@ namespace BTCPayServer.Services.Mails
|
||||||
{
|
{
|
||||||
public class EmailSettings
|
public class EmailSettings
|
||||||
{
|
{
|
||||||
[Required]
|
|
||||||
public string Server
|
public string Server
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
|
||||||
public int? Port
|
public int? Port
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
|
||||||
public String Login
|
public String Login
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
|
||||||
public String Password
|
public String Password
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string From
|
public string From
|
||||||
{
|
{
|
||||||
|
@ -45,6 +40,18 @@ namespace BTCPayServer.Services.Mails
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsComplete()
|
||||||
|
{
|
||||||
|
SmtpClient smtp = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
smtp = CreateSmtpClient();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public SmtpClient CreateSmtpClient()
|
public SmtpClient CreateSmtpClient()
|
||||||
{
|
{
|
||||||
SmtpClient client = new SmtpClient(Server, Port.Value);
|
SmtpClient client = new SmtpClient(Server, Port.Value);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -8,12 +9,14 @@ namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
public class PoliciesSettings
|
public class PoliciesSettings
|
||||||
{
|
{
|
||||||
|
[Display(Name = "Requires a confirmation mail for registering")]
|
||||||
public bool RequiresConfirmedEmail
|
public bool RequiresConfirmedEmail
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||||
|
[Display(Name = "Disable registration")]
|
||||||
public bool LockSubscription { get; set; }
|
public bool LockSubscription { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,40 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using ExchangeSharp;
|
||||||
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;
|
||||||
|
|
||||||
public IMemoryCache Cache
|
public IMemoryCache Cache
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -20,18 +43,73 @@ 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()
|
||||||
|
{
|
||||||
|
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
|
||||||
|
DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
|
||||||
|
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
|
||||||
|
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
|
||||||
|
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
|
||||||
|
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
|
||||||
|
|
||||||
|
// Handmade providers
|
||||||
|
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
|
||||||
|
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||||
|
DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, Authenticator = _CoinAverageSettings });
|
||||||
|
|
||||||
|
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||||
|
//DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true));
|
||||||
|
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||||
|
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
|
||||||
|
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
|
||||||
|
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
|
||||||
|
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CoinAverageExchanges GetSupportedExchanges()
|
||||||
|
{
|
||||||
|
CoinAverageExchanges exchanges = new CoinAverageExchanges();
|
||||||
|
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
|
||||||
|
{
|
||||||
|
exchanges.Add(exchange.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add other exchanges supported here
|
||||||
|
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||||
|
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||||
|
|
||||||
|
return exchanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +129,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()
|
||||||
|
{
|
||||||
|
Exchange = exchangeName,
|
||||||
|
Authenticator = _CoinAverageSettings
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
var fallback = new FallbackRateProvider(providers.ToArray());
|
||||||
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
|
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
|
||||||
coinAverage.Exchange = exchange;
|
|
||||||
providers.Add(coinAverage);
|
|
||||||
return new FallbackRateProvider(providers.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
|
|
||||||
{
|
|
||||||
return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope };
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRateProvider GetDefaultRateProvider(BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
if(network.DefaultRateProvider == null)
|
|
||||||
{
|
{
|
||||||
throw new RateUnavailableException(network.CryptoCode);
|
CacheSpan = CacheSpan
|
||||||
}
|
};
|
||||||
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
|
var value = await cached.GetRatesAsync();
|
||||||
|
return new QueryRateResult()
|
||||||
|
{
|
||||||
|
CachedResult = !fallback.Used,
|
||||||
|
ExchangeRates = value,
|
||||||
|
Exceptions = fallback.Exceptions
|
||||||
|
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,31 +31,22 @@ namespace BTCPayServer.Services.Rates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ExchangeName { get; set; }
|
||||||
|
|
||||||
public TimeSpan CacheSpan
|
public TimeSpan CacheSpan
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = 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)
|
|
||||||
{
|
|
||||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
|
||||||
{
|
|
||||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
|
||||||
return _Inner.GetRateAsync(currency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<ICollection<Rate>> GetRatesAsync()
|
public Task<ExchangeRates> GetRatesAsync()
|
||||||
{
|
{
|
||||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||||
{
|
{
|
||||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
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
|
||||||
|
@ -69,18 +47,18 @@ namespace BTCPayServer.Services.Rates
|
||||||
public interface ICoinAverageAuthenticator
|
public interface ICoinAverageAuthenticator
|
||||||
{
|
{
|
||||||
Task AddHeader(HttpRequestMessage message);
|
Task AddHeader(HttpRequestMessage message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CoinAverageRateProvider : IRateProvider
|
public class CoinAverageRateProvider : IRateProvider
|
||||||
{
|
{
|
||||||
|
public const string CoinAverageName = "coinaverage";
|
||||||
|
public CoinAverageRateProvider()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
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,27 +66,19 @@ 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"];
|
||||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExchangeRates> GetRatesAsync()
|
||||||
|
{
|
||||||
|
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||||
|
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
var auth = Authenticator;
|
var auth = Authenticator;
|
||||||
|
@ -128,36 +98,29 @@ 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))
|
var exchangeRates = new ExchangeRates();
|
||||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
|
foreach (var prop in rates.Properties())
|
||||||
{
|
{
|
||||||
TryToDecimal(p, out decimal v);
|
ExchangeRate exchangeRate = new ExchangeRate();
|
||||||
return v;
|
exchangeRate.Exchange = Exchange;
|
||||||
});
|
if (!TryToDecimal(prop, out decimal value))
|
||||||
|
continue;
|
||||||
|
exchangeRate.Value = value;
|
||||||
|
if(CurrencyPair.TryParse(prop.Name, out var pair))
|
||||||
|
{
|
||||||
|
exchangeRate.CurrencyPair = pair;
|
||||||
|
exchangeRates.Add(exchangeRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exchangeRates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryToDecimal(JProperty p, out decimal v)
|
|
||||||
{
|
|
||||||
JToken token = p.Value[Exchange == null ? "last" : "bid"];
|
|
||||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
|
||||||
{
|
|
||||||
var rates = await GetRatesCore();
|
|
||||||
return rates.Select(o => new Rate()
|
|
||||||
{
|
|
||||||
Currency = o.Key,
|
|
||||||
Value = o.Value
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TestAuthAsync()
|
public async Task TestAuthAsync()
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
|
||||||
|
@ -217,7 +180,7 @@ namespace BTCPayServer.Services.Rates
|
||||||
var exchanges = (JObject)jobj["exchanges"];
|
var exchanges = (JObject)jobj["exchanges"];
|
||||||
response.Exchanges = exchanges
|
response.Exchanges = exchanges
|
||||||
.Properties()
|
.Properties()
|
||||||
.Select(p =>
|
.Select(p =>
|
||||||
{
|
{
|
||||||
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
|
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
|
||||||
exchange.Name = p.Name;
|
exchange.Name = p.Name;
|
||||||
|
|
|
@ -20,12 +20,42 @@ 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 string Url
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
|
||||||
|
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
||||||
|
{
|
||||||
|
public CoinAverageExchanges()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(CoinAverageExchange exchange)
|
||||||
|
{
|
||||||
|
TryAdd(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 +67,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 +120,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)
|
||||||
|
|
|
@ -40,6 +40,14 @@ namespace BTCPayServer.Services.Rates
|
||||||
}
|
}
|
||||||
|
|
||||||
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||||
|
|
||||||
|
public NumberFormatInfo GetNumberFormatInfo(string currency)
|
||||||
|
{
|
||||||
|
var data = GetCurrencyProvider(currency);
|
||||||
|
if (data is NumberFormatInfo nfi)
|
||||||
|
return nfi;
|
||||||
|
return ((CultureInfo)data).NumberFormat;
|
||||||
|
}
|
||||||
public IFormatProvider GetCurrencyProvider(string currency)
|
public IFormatProvider GetCurrencyProvider(string currency)
|
||||||
{
|
{
|
||||||
lock (_CurrencyProviders)
|
lock (_CurrencyProviders)
|
||||||
|
@ -54,7 +62,11 @@ namespace BTCPayServer.Services.Rates
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
AddCurrency(_CurrencyProviders, "BTC", 8, "BTC");
|
|
||||||
|
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||||
|
{
|
||||||
|
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _CurrencyProviders.TryGet(currency);
|
return _CurrencyProviders.TryGet(currency);
|
||||||
}
|
}
|
||||||
|
@ -106,6 +118,17 @@ namespace BTCPayServer.Services.Rates
|
||||||
info.Symbol = splitted[3];
|
info.Symbol = splitted[3];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||||
|
{
|
||||||
|
dico.TryAdd(network.CryptoCode, new CurrencyData()
|
||||||
|
{
|
||||||
|
Code = network.CryptoCode,
|
||||||
|
Divisibility = 8,
|
||||||
|
Name = network.CryptoCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return dico.Values.ToArray();
|
return dico.Values.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
74
BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs
Normal file
74
BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using ExchangeSharp;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services.Rates
|
||||||
|
{
|
||||||
|
public class ExchangeSharpRateProvider : IRateProvider
|
||||||
|
{
|
||||||
|
readonly ExchangeAPI _ExchangeAPI;
|
||||||
|
readonly string _ExchangeName;
|
||||||
|
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
|
||||||
|
{
|
||||||
|
if (exchangeAPI == null)
|
||||||
|
throw new ArgumentNullException(nameof(exchangeAPI));
|
||||||
|
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
|
||||||
|
_ExchangeAPI = exchangeAPI;
|
||||||
|
_ExchangeName = exchangeName;
|
||||||
|
ReverseCurrencyPair = reverseCurrencyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ReverseCurrencyPair
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExchangeRates> GetRatesAsync()
|
||||||
|
{
|
||||||
|
await new SynchronizationContextRemover();
|
||||||
|
var rates = await _ExchangeAPI.GetTickersAsync();
|
||||||
|
lock (notFoundSymbols)
|
||||||
|
{
|
||||||
|
var exchangeRates =
|
||||||
|
rates.Select(t => CreateExchangeRate(t))
|
||||||
|
.Where(t => t != null)
|
||||||
|
.ToArray();
|
||||||
|
return new ExchangeRates(exchangeRates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||||
|
HashSet<string> notFoundSymbols = new HashSet<string>();
|
||||||
|
private ExchangeRate CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
|
||||||
|
{
|
||||||
|
if (notFoundSymbols.Contains(ticker.Key))
|
||||||
|
return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key);
|
||||||
|
if (!CurrencyPair.TryParse(tickerName, out var pair))
|
||||||
|
{
|
||||||
|
notFoundSymbols.Add(ticker.Key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(ReverseCurrencyPair)
|
||||||
|
pair = new CurrencyPair(pair.Right, pair.Left);
|
||||||
|
var rate = new ExchangeRate();
|
||||||
|
rate.CurrencyPair = pair;
|
||||||
|
rate.Exchange = _ExchangeName;
|
||||||
|
rate.Value = ticker.Value.Bid;
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
notFoundSymbols.Add(ticker.Key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue