Merge remote-tracking branch 'source/master' into dev-lndrpc

This commit is contained in:
rockstardev 2018-05-16 04:50:46 -05:00
commit 6cefd9c3e7
152 changed files with 5599 additions and 1639 deletions

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

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

View file

@ -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());
}
}
}

View file

@ -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 :

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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()
{ {

View file

@ -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),

View 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'")
});
}
}
}

View file

@ -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'")

View file

@ -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),

View 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'")
});
}
}
}

View file

@ -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;
} }
} }

View file

@ -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>

View file

@ -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;

View file

@ -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
{ {

View file

@ -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
{ {

View file

@ -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();
} }
} }

View file

@ -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 });
}
} }
} }

View file

@ -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();

View file

@ -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)
{ {

View file

@ -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))

View file

@ -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
{ {

View file

@ -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;
}
}
} }
} }

View file

@ -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);

View file

@ -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;
} }

View file

@ -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);

View file

@ -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);
} }
} }

View file

@ -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);

View 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;
}
}
}

View file

@ -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);

View file

@ -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);
} }
} }
} }

View file

@ -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;
} }
} }
} }

View file

@ -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)

View file

@ -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 + "');");
}
} }
} }

View file

@ -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;
} }
} }

View file

@ -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();

View file

@ -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());
})); }));

View file

@ -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)

View file

@ -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);

View file

@ -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);
} }

View file

@ -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 =>
{ {

View file

@ -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;
}
} }
} }

View file

@ -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();

View file

@ -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)

View file

@ -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 =>
{ {

View file

@ -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 =>
{ {

View file

@ -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 =>
{ {

View file

@ -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 =>
{ {

View file

@ -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 =>
{ {

View file

@ -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)
}, },

View 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
}
}
}

View 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");
}
}
}

View file

@ -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")

View file

@ -19,5 +19,6 @@ namespace BTCPayServer.Models
{ {
get; set; get; set;
} }
public string ButtonClass { get; set; } = "btn-danger";
} }
} }

View file

@ -1,11 +0,0 @@
using System;
namespace BTCPayServer.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

View file

@ -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;
} }
} }
} }

View file

@ -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;

View file

@ -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
{ {

View file

@ -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;

View file

@ -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; }

View file

@ -18,8 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
{ {
get; set; get; set;
} }
[Required]
[EmailAddress] [EmailAddress]
public string TestEmail public string TestEmail
{ {

View file

@ -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();

View 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}";
}
}
}
}

View file

@ -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;
}
} }
} }

View file

@ -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; }
} }
} }

View file

@ -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;

View file

@ -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;

View file

@ -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" };
} }

View file

@ -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)

View file

@ -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);

View 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);
}
}
}

View 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)}";
}
}
}

View 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;
}
}
}
}

View 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());
}
}
}
}
}
}
}
}

View 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;
}
}
}

View 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";
}
}
}

View file

@ -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;

View file

@ -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;
} }
} }

View file

@ -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);
} }

View file

@ -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;

View file

@ -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"),
}; };
} }
} }

View file

@ -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);

View file

@ -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);

View file

@ -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; }
} }
} }

View file

@ -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()
};
} }
} }
} }

View file

@ -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());
} }
} }
} }

View file

@ -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; }
} }
} }

View file

@ -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;

View file

@ -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)

View file

@ -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();
} }

View 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;
}
}
}
}

View file

@ -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>();
} }
} }

View file

@ -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();
} }
} }

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more