Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
272ac49872 | |||
5f05ca5ac6 | |||
7872b3ec55 | |||
27a0aebd12 | |||
366490516e | |||
9a92646d4d | |||
b002c49dac | |||
3f4ec9ba80 | |||
0290a5eacd | |||
744734a6a1 | |||
29f662f87c | |||
af21f9f10c | |||
efdc99b9d1 | |||
4458e63c1a | |||
3225745115 | |||
a325592106 | |||
01069ed583 | |||
0fc770bbb1 | |||
dfb79ef96e | |||
4ebffc8d43 | |||
c2dad08fef | |||
c3d73236e0 | |||
8a4da361fd | |||
57effe318b | |||
9325441693 | |||
180341576b | |||
e2533a93e3 | |||
14360bde78 | |||
d793265bed | |||
0a449e1e8e | |||
74ccc34c9c | |||
674cd1486d | |||
ce12e87b70 | |||
8f1324fdf3 | |||
3ab69046b0 | |||
6dc4bfaefe | |||
f460837f96 | |||
34d0d3e011 | |||
e57a488371 | |||
43be1e191f | |||
eb975bf8fc | |||
21bbf49640 | |||
9339c7dff2 | |||
af0eb831a2 | |||
1fc9a1a54b | |||
3954ce2137 | |||
271de362cb | |||
d41474ebc8 | |||
5b0b3e30f4 | |||
48a95457b6 | |||
7c0b26174f | |||
f0145142a4 | |||
2848caff2e | |||
9e05ad787f | |||
de39fa0aea | |||
94ff77f2b2 | |||
bb7dc1ed4a | |||
c5e833ee79 | |||
4397591134 | |||
986c7b94f4 | |||
a6ef7387cf | |||
95bdeacd93 | |||
07c2f6b810 | |||
8ff81f1648 | |||
c3ee43c228 | |||
d85da28ca7 | |||
042142396d | |||
fbc4ca89aa | |||
2e5d29064b | |||
ef0b8376d3 | |||
1fa1b74261 |
@ -2,8 +2,11 @@
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -104,15 +107,6 @@ namespace BTCPayServer.Tests
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
if (MockRates)
|
||||
{
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
|
||||
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
|
||||
mockRates.AddMock(btc);
|
||||
mockRates.AddMock(ltc);
|
||||
s.AddSingleton<IRateProviderFactory>(mockRates);
|
||||
}
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
@ -126,6 +120,30 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
|
||||
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
|
||||
rateProvider.DirectProviders.Clear();
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
Value = 5000m
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
Value = 4500m
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
Value = 500m
|
||||
});
|
||||
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
|
||||
}
|
||||
|
||||
public string HostName
|
||||
@ -142,7 +160,7 @@ namespace BTCPayServer.Tests
|
||||
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();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
@ -150,7 +168,11 @@ namespace BTCPayServer.Tests
|
||||
context.Request.Protocol = "http";
|
||||
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 provider = scope.CreateScope().ServiceProvider;
|
||||
|
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
}
|
||||
}
|
134
BTCPayServer.Tests/RateRulesTest.cs
Normal file
134
BTCPayServer.Tests/RateRulesTest.cs
Normal file
@ -0,0 +1,134 @@
|
||||
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: "BTC_USD", Expected: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||
(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: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||
(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);
|
||||
////////
|
||||
}
|
||||
}
|
||||
}
|
@ -44,29 +44,27 @@ namespace BTCPayServer.Tests
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
await RegisterAsync();
|
||||
var store = await CreateStoreAsync();
|
||||
await CreateStoreAsync();
|
||||
var store = this.GetController<StoresController>();
|
||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
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" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
var store2 = parent.PayTester.GetController<StoresController>(UserId);
|
||||
store2.CreatedStoreId = store.CreatedStoreId;
|
||||
return store2;
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
@ -78,12 +76,12 @@ namespace BTCPayServer.Tests
|
||||
public async Task RegisterDerivationSchemeAsync(string 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);
|
||||
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;
|
||||
await store.UpdateStore(StoreId, vm);
|
||||
await store.UpdateStore(vm);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
@ -127,7 +125,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
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()
|
||||
{
|
||||
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
||||
|
@ -32,6 +32,11 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -105,22 +110,11 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
#pragma warning disable CS0618
|
||||
entity.TxFee = Money.Coins(0.1m);
|
||||
entity.Rate = 5000;
|
||||
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
|
||||
// Some check that handling legacy stuff does not break things
|
||||
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
|
||||
paymentMethod.Calculate();
|
||||
Assert.NotNull(paymentMethod);
|
||||
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
|
||||
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
////////////////////
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
@ -235,6 +229,73 @@ namespace BTCPayServer.Tests
|
||||
#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.Coins(0), 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.0,
|
||||
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 CanPayUsingBIP70()
|
||||
{
|
||||
@ -303,9 +364,9 @@ namespace BTCPayServer.Tests
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult());
|
||||
var storeController = user.GetController<StoresController>();
|
||||
Assert.IsType<ViewResult>(storeController.UpdateStore());
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
@ -319,7 +380,7 @@ namespace BTCPayServer.Tests
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "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)));
|
||||
}
|
||||
}
|
||||
@ -465,8 +526,8 @@ namespace BTCPayServer.Tests
|
||||
acc.Register();
|
||||
acc.CreateStore();
|
||||
|
||||
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
|
||||
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
|
||||
var controller = acc.GetController<StoresController>();
|
||||
var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "bla",
|
||||
@ -524,13 +585,15 @@ namespace BTCPayServer.Tests
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.Register();
|
||||
var store = acc.CreateStore();
|
||||
acc.CreateStore();
|
||||
var store = acc.GetController<StoresController>();
|
||||
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
|
||||
|
||||
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
||||
var store2 = acc.CreateStore();
|
||||
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
|
||||
acc.CreateStore();
|
||||
var store2 = acc.GetController<StoresController>();
|
||||
store2.Pair(pairingCode.ToString(), store2.StoreData.Id).GetAwaiter().GetResult();
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
@ -623,6 +686,36 @@ namespace BTCPayServer.Tests
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
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.0,
|
||||
Currency = "USD"
|
||||
};
|
||||
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
|
||||
var result = client.SendAsync(message).GetAwaiter().GetResult();
|
||||
result.EnsureSuccessStatusCode();
|
||||
/////////////////////
|
||||
}
|
||||
}
|
||||
|
||||
@ -652,10 +745,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
||||
{
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
vm.PreferredExchange = exchange;
|
||||
storeController.UpdateStore(user.StoreId, vm).Wait();
|
||||
storeController.Rates(vm).Wait();
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
@ -691,11 +784,11 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
Assert.Equal(1.0, vm.RateMultiplier);
|
||||
vm.RateMultiplier = 0.5;
|
||||
storeController.UpdateStore(user.StoreId, vm).Wait();
|
||||
storeController.Rates(vm).Wait();
|
||||
|
||||
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
@ -771,6 +864,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]
|
||||
public void CanPayWithTwoCurrencies()
|
||||
{
|
||||
@ -930,10 +1083,10 @@ namespace BTCPayServer.Tests
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
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.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()
|
||||
{
|
||||
@ -1093,8 +1246,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var txFee = Money.Zero;
|
||||
|
||||
var rate = user.BitPay.GetRates();
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
@ -1198,40 +1349,103 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CheckQuadrigacxRateProvider()
|
||||
{
|
||||
var quadri = new QuadrigacxRateProvider("BTC");
|
||||
var quadri = new QuadrigacxRateProvider();
|
||||
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
|
||||
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
|
||||
}
|
||||
|
||||
quadri = new QuadrigacxRateProvider("LTC");
|
||||
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
||||
[Fact]
|
||||
public void CanQueryDirectProviders()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
|
||||
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]
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
var coinAverage = new CoinAverageRateProvider("BTC");
|
||||
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var coinAverage = new CoinAverageRateProvider();
|
||||
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
|
||||
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
|
||||
|
||||
var cached = new CachedRateProvider("BTC", coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
|
||||
cached.CacheSpan = TimeSpan.FromSeconds(10);
|
||||
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
//Manually check that cache get hit after 10 sec
|
||||
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
|
||||
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
factory.DirectProviders.Clear();
|
||||
factory.CacheSpan = TimeSpan.FromSeconds(10);
|
||||
|
||||
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.False(fetchedRate.Cached);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.True(fetchedRate.Cached);
|
||||
|
||||
Thread.Sleep(11000);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.False(fetchedRate.Cached);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.True(fetchedRate.Cached);
|
||||
// Should cache at exchange level so this should hit the cache
|
||||
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.True(fetchedRate.Cached);
|
||||
Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value);
|
||||
|
||||
// Should cache at exchange level this should not hit the cache as it is different exchange
|
||||
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.False(fetchedRate.Cached);
|
||||
|
||||
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
|
||||
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
|
||||
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
|
@ -1,52 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ services:
|
||||
- lightning-charged
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.2
|
||||
image: nicolasdorier/nbxplorer:1.0.2.3
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -89,7 +89,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.9-dev
|
||||
image: nicolasdorier/clightning:0.0.0.12-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -130,7 +130,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.9-dev
|
||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
|
@ -1,35 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitIdentity : IIdentity
|
||||
{
|
||||
public BitIdentity(PubKey key)
|
||||
{
|
||||
PubKey = key;
|
||||
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
|
||||
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
|
||||
}
|
||||
string _Name;
|
||||
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
}
|
||||
public PubKey PubKey
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string AuthenticationType => "BitID";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name => _Name;
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ namespace BTCPayServer.Authentication
|
||||
|
||||
public async Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
if (sin == null)
|
||||
return Array.Empty<BitTokenEntity>();
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData
|
||||
@ -43,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)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
|
@ -44,7 +44,6 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public RateProviderDescription DefaultRateProvider { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
@ -62,6 +61,7 @@ namespace BTCPayServer
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -14,9 +14,6 @@ namespace BTCPayServer
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProviderDescription("BTC");
|
||||
var bitpay = new BitpayRateProviderDescription();
|
||||
var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
@ -24,7 +21,6 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -20,7 +20,11 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DOGE_X = DOGE_BTC * BTC_X",
|
||||
"DOGE_BTC = bittrex(DOGE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
|
@ -20,7 +20,6 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -86,7 +86,11 @@ namespace BTCPayServer
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.93</Version>
|
||||
<Version>1.0.2.6</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -31,6 +31,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
@ -39,10 +40,10 @@
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.3" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.4" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.4" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
|
@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[BitpayAPIConstraint]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -23,7 +24,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> Tokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
|
||||
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
|
||||
@ -51,7 +52,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
|
@ -16,10 +16,11 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
|
@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type.Value.ToString() != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
|
@ -22,20 +22,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
@ -44,21 +38,16 @@ namespace BTCPayServer.Controllers
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
@ -77,8 +66,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
@ -88,54 +76,13 @@ namespace BTCPayServer.Controllers
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { store.Id }
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
|
||||
{
|
||||
if (facade == null)
|
||||
throw new ArgumentNullException(nameof(facade));
|
||||
|
||||
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().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} for SIN {this.GetBitIdentity().SIN}");
|
||||
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(bitToken.StoreId);
|
||||
if (store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
return store;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -74,6 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||
cryptoPayment.Due = accounting.Due.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;
|
||||
if (onchainMethod != null)
|
||||
@ -226,6 +228,7 @@ namespace BTCPayServer.Controllers
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
@ -355,7 +358,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
@ -368,13 +371,15 @@ namespace BTCPayServer.Controllers
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
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
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
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",
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
@ -390,11 +395,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
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)
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
@ -405,18 +410,23 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
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)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
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");
|
||||
return View(model);
|
||||
@ -454,20 +464,15 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
@ -481,7 +486,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
|
@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
IRateProviderFactory _RateProviders;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviders,
|
||||
BTCPayRateProviderFactory rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_UserManager = userManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
@ -111,6 +113,23 @@ namespace BTCPayServer.Controllers
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
@ -119,19 +138,45 @@ namespace BTCPayServer.Controllers
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
|
||||
.ToList();
|
||||
|
||||
List<string> paymentMethodErrors = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
foreach(var pair in fetchingByCurrencyPair)
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
bool hasError = false;
|
||||
if(rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
hasError = true;
|
||||
}
|
||||
if(rateResult.ExchangeExceptions.Count != 0)
|
||||
{
|
||||
foreach(var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
hasError = true;
|
||||
}
|
||||
if(hasError)
|
||||
{
|
||||
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
|
||||
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
|
||||
throw new PaymentMethodUnavailableException("Payment method unavailable");
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
@ -158,23 +203,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
#pragma warning disable CS0618
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
||||
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
|
||||
{
|
||||
var btc = _NetworkProvider.BTC;
|
||||
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
|
||||
var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
|
||||
if (feeProvider != null && rateProvider != null)
|
||||
{
|
||||
var gettingFee = feeProvider.GetFeeRateAsync();
|
||||
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
|
||||
entity.Rate = await gettingRate;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||
|
||||
@ -183,15 +211,17 @@ namespace BTCPayServer.Controllers
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
if (rate.Value == null)
|
||||
return null;
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
paymentMethod.Rate = rate.Value.Value;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
@ -217,16 +247,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (compare != null)
|
||||
{
|
||||
var limitValueRate = 0.0m;
|
||||
if (limitValue.Currency == entity.ProductInformation.Currency)
|
||||
limitValueRate = paymentMethod.Rate;
|
||||
else
|
||||
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
|
||||
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
|
||||
if (limitValueRate.Value.HasValue)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(errorMessage);
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
@ -243,13 +271,6 @@ namespace BTCPayServer.Controllers
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if (transactionSpeed == null)
|
||||
|
@ -21,10 +21,11 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
{
|
||||
|
@ -8,17 +8,19 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
@ -32,45 +34,90 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
RateRules rules = null;
|
||||
if (storeId != null)
|
||||
if(storeId == null || currencyPairs == null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rules = store.GetStoreBlob().GetRateRules();
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
result.StatusCode = 404;
|
||||
return result;
|
||||
}
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
|
||||
foreach(var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if(!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
CryptoCode = r.Pair.Left,
|
||||
Code = r.Pair.Right,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
|
||||
Value = r.Value.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
|
||||
public class Rate
|
||||
{
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "cryptoCode")]
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "rate")]
|
||||
public decimal Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,23 +19,20 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Roles = Roles.ServerAdmin)]
|
||||
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||
public class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
private CssThemeManager _CssThemeManager;
|
||||
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
CssThemeManager cssThemeManager)
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -102,7 +99,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider("BTC")
|
||||
return new CoinAverageRateProvider()
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
@ -234,9 +231,6 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Theme(ThemeSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
// TODO: remove controller/class-level property and have only reference to
|
||||
// CssThemeManager here in this method
|
||||
_CssThemeManager.Update(settings);
|
||||
TempData["StatusMessage"] = "Theme settings updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
@ -247,10 +241,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (command == "Test")
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
try
|
||||
{
|
||||
if(!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
var client = model.Settings.CreateSmtpClient();
|
||||
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
@ -261,11 +258,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
else // if(command == "Save")
|
||||
{
|
||||
ModelState.Remove(nameof(model.TestEmail));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
model.StatusMessage = "Email settings saved";
|
||||
return View(model);
|
||||
|
@ -21,9 +21,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[HttpGet]
|
||||
[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)
|
||||
return NotFound();
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
@ -60,7 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
@ -188,7 +188,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
|
@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[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)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
@ -4,6 +4,8 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -19,6 +21,7 @@ using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -27,11 +30,12 @@ using System.Threading.Tasks;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = StorePolicies.OwnStore)]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateFactory;
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
@ -45,12 +49,14 @@ namespace BTCPayServer.Controllers
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayRateProviderFactory rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
@ -93,13 +99,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[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();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.ServerUrl = GetStoreUrl(StoreData.Id);
|
||||
model.CryptoCurrency = cryptoCode;
|
||||
return View(model);
|
||||
}
|
||||
@ -111,17 +114,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId)
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
await FillUsers(storeId, vm);
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(storeId);
|
||||
vm.StoreId = storeId;
|
||||
var users = await _Repo.GetStoreUsers(StoreData.Id);
|
||||
vm.StoreId = StoreData.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
@ -130,11 +133,20 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public StoreData StoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[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)
|
||||
{
|
||||
return View(vm);
|
||||
@ -150,7 +162,7 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
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");
|
||||
return View(vm);
|
||||
@ -161,19 +173,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[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();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
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"
|
||||
});
|
||||
}
|
||||
@ -188,15 +197,151 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId)
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
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();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
@ -204,12 +349,13 @@ namespace BTCPayServer.Controllers
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
|
||||
public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
|
||||
{
|
||||
CurrencyValue lightningMaxValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||
@ -228,16 +374,12 @@ namespace BTCPayServer.Controllers
|
||||
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;
|
||||
var blob = store.GetStoreBlob();
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
@ -253,33 +395,33 @@ namespace BTCPayServer.Controllers
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
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);
|
||||
if (store.SetStoreBlob(blob))
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(CheckoutExperience), new
|
||||
{
|
||||
storeId = storeId
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[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)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
@ -288,8 +430,8 @@ namespace BTCPayServer.Controllers
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -329,78 +471,57 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
public async Task<IActionResult> UpdateStore(StoreViewModel model)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
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);
|
||||
AddPaymentMethods(StoreData, model);
|
||||
|
||||
bool needUpdate = false;
|
||||
if (store.SpeedPolicy != model.SpeedPolicy)
|
||||
if (StoreData.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
StoreData.SpeedPolicy = model.SpeedPolicy;
|
||||
}
|
||||
if (store.StoreName != model.StoreName)
|
||||
if (StoreData.StoreName != model.StoreName)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreName = model.StoreName;
|
||||
StoreData.StoreName = model.StoreName;
|
||||
}
|
||||
if (store.StoreWebsite != model.StoreWebsite)
|
||||
if (StoreData.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
StoreData.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
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)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId = storeId
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
}
|
||||
|
||||
private (String DisplayName, String Name)[] GetSupportedExchanges()
|
||||
private CoinAverageExchange[] GetSupportedExchanges()
|
||||
{
|
||||
return new[] { ("Coin Average", "coinaverage") }.Concat(_CoinAverage.AvailableExchanges).ToArray();
|
||||
return _CoinAverage.AvailableExchanges
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
@ -412,10 +533,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/Tokens")]
|
||||
public async Task<IActionResult> ListTokens(string storeId)
|
||||
public async Task<IActionResult> ListTokens()
|
||||
{
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId);
|
||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
|
||||
model.StatusMessage = StatusMessage;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
@ -424,30 +545,43 @@ namespace BTCPayServer.Controllers
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
}).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);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken(CreateTokenViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
model.Label = model.Label ?? String.Empty;
|
||||
storeId = model.StoreId ?? storeId;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Unauthorized();
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
|
||||
var store = StoreData;
|
||||
var storeId = StoreData?.Id;
|
||||
if (storeId == null)
|
||||
{
|
||||
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
storeId = model.StoreId;
|
||||
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()
|
||||
@ -488,11 +622,20 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId)
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
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();
|
||||
model.Facade = "merchant";
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
@ -501,20 +644,25 @@ namespace BTCPayServer.Controllers
|
||||
model.StoreId = storeId;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[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);
|
||||
if (token == null ||
|
||||
token.StoreId != storeId ||
|
||||
token.StoreId != StoreData.Id ||
|
||||
!await _TokenRepository.DeleteToken(tokenId))
|
||||
StatusMessage = "Failure to revoke this token";
|
||||
else
|
||||
@ -522,11 +670,26 @@ namespace BTCPayServer.Controllers
|
||||
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]
|
||||
[Route("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
@ -537,7 +700,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
@ -545,7 +708,7 @@ namespace BTCPayServer.Controllers
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
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,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
@ -556,19 +719,22 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
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);
|
||||
if (store == null || pairing == null)
|
||||
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 RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
return Challenge(Policies.CookieAuthentication);
|
||||
}
|
||||
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
@ -594,6 +760,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
if (User.Identity.AuthenticationType != Policies.CookieAuthentication)
|
||||
return null;
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -15,7 +16,7 @@ using NBXplorer.DerivationStrategy;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UserStoresController : Controller
|
||||
{
|
||||
@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
[HttpGet]
|
||||
[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)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.Role == StoreRoles.Owner,
|
||||
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
|
||||
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
|
23
BTCPayServer/Data/APIKeyData.cs
Normal file
23
BTCPayServer/Data/APIKeyData.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class APIKeyData
|
||||
{
|
||||
[MaxLength(50)]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
@ -86,6 +86,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||
@ -112,6 +117,8 @@ namespace BTCPayServer.Data
|
||||
t.StoreDataId
|
||||
});
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(a => a.StoreData);
|
||||
|
@ -14,6 +14,11 @@ using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -120,7 +125,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
@ -151,10 +156,35 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
[Obsolete]
|
||||
public string Role
|
||||
{
|
||||
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
|
||||
{
|
||||
get;
|
||||
@ -178,7 +208,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public StoreBlob GetStoreBlob()
|
||||
{
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
if (result.PreferredExchange == null)
|
||||
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool SetStoreBlob(StoreBlob storeBlob)
|
||||
@ -192,9 +225,9 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
public class RateRule_Obsolete
|
||||
{
|
||||
public RateRule()
|
||||
public RateRule_Obsolete()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
@ -214,6 +247,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
@ -246,8 +280,8 @@ namespace BTCPayServer.Data
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule>();
|
||||
RateRules.Add(new RateRule() { Multiplier = rate });
|
||||
RateRules = new List<RateRule_Obsolete>();
|
||||
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
|
||||
}
|
||||
public decimal GetRateMultiplier()
|
||||
{
|
||||
@ -261,7 +295,7 @@ namespace BTCPayServer.Data
|
||||
return rate;
|
||||
}
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
@ -273,6 +307,11 @@ namespace BTCPayServer.Data
|
||||
public Uri CustomLogo { get; set; }
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomCSS { get; set; }
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
public bool RateScripting { get; set; }
|
||||
|
||||
public string RateScript { get; set; }
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Models;
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -102,12 +104,6 @@ namespace BTCPayServer
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
@ -133,6 +129,14 @@ namespace BTCPayServer
|
||||
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)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -142,12 +146,52 @@ namespace BTCPayServer
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
|
||||
public static string GetSIN(this ClaimsPrincipal principal)
|
||||
{
|
||||
if (!(controller.User.Identity is BitIdentity))
|
||||
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
|
||||
return (BitIdentity)controller.User.Identity;
|
||||
return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static string GetStoreId(this ClaimsPrincipal principal)
|
||||
{
|
||||
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() };
|
||||
@ -156,13 +200,5 @@ namespace BTCPayServer
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static HtmlString ToJSVariableModel(this object o, string variableName)
|
||||
{
|
||||
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
|
||||
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -43,9 +43,7 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
|
||||
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
|
||||
return (hasVersion || hasIdentity) == IsBitpayAPI;
|
||||
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
|
||||
}
|
||||
}
|
||||
|
||||
|
66
BTCPayServer/HostedServices/BaseAsyncService.cs
Normal file
66
BTCPayServer/HostedServices/BaseAsyncService.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public abstract class BaseAsyncService : IHostedService
|
||||
{
|
||||
private CancellationTokenSource _Cts;
|
||||
protected Task[] _Tasks;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = new CancellationTokenSource();
|
||||
_Tasks = InitializeTasks();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal abstract Task[] InitializeTasks();
|
||||
|
||||
protected CancellationToken Cancellation
|
||||
{
|
||||
get { return _Cts.Token; }
|
||||
}
|
||||
|
||||
protected async Task CreateLoopTask(Func<Task> act, [CallerMemberName]string caller = null)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, caller + " failed");
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), _Cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.IsCancellationRequested) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,47 +16,59 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CssThemeManager
|
||||
{
|
||||
public CssThemeManager(SettingsRepository settingsRepository)
|
||||
{
|
||||
Update(settingsRepository);
|
||||
}
|
||||
|
||||
private async void Update(SettingsRepository settingsRepository)
|
||||
{
|
||||
var data = (await settingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
Update(data);
|
||||
}
|
||||
|
||||
public void Update(ThemeSettings data)
|
||||
{
|
||||
UpdateBootstrap(data.BootstrapCssUri);
|
||||
UpdateCreativeStart(data.CreativeStartCssUri);
|
||||
}
|
||||
if (String.IsNullOrWhiteSpace(data.BootstrapCssUri))
|
||||
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_bootstrapUri = data.BootstrapCssUri;
|
||||
|
||||
private string _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(data.CreativeStartCssUri))
|
||||
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_creativeStartUri = data.CreativeStartCssUri;
|
||||
}
|
||||
|
||||
private string _bootstrapUri;
|
||||
public string BootstrapUri
|
||||
{
|
||||
get { return _bootstrapUri; }
|
||||
}
|
||||
public void UpdateBootstrap(string newUri)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(newUri))
|
||||
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v="+ DateTime.Now.Ticks;
|
||||
else
|
||||
_bootstrapUri = newUri;
|
||||
}
|
||||
|
||||
private string _creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
private string _creativeStartUri;
|
||||
public string CreativeStartUri
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
public void UpdateCreativeStart(string newUri)
|
||||
}
|
||||
|
||||
public class CssThemeManagerHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CssThemeManager _CssThemeManager;
|
||||
|
||||
public CssThemeManagerHostedService(SettingsRepository settingsRepository, CssThemeManager cssThemeManager)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(newUri))
|
||||
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_creativeStartUri = newUri;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(ListenForThemeChanges)
|
||||
};
|
||||
}
|
||||
|
||||
async Task ListenForThemeChanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
_CssThemeManager.Update(data);
|
||||
|
||||
await _SettingsRepository.WaitSettingsChanged<ThemeSettings>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +305,10 @@ namespace BTCPayServer.HostedServices
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
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.
|
||||
if (invoice.FullNotifications)
|
||||
@ -315,20 +318,22 @@ namespace BTCPayServer.HostedServices
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
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")
|
||||
{
|
||||
await Notify(invoice);
|
||||
tasks.Add(Notify(invoice));
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
tasks.Add(Notify(invoice, e.EventCode, e.Name));
|
||||
}
|
||||
await Task.WhenAll(tasks.ToArray());
|
||||
}));
|
||||
|
||||
|
||||
|
@ -68,6 +68,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
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();
|
||||
@ -78,7 +80,7 @@ namespace BTCPayServer.HostedServices
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
if (accounting.Paid >= accounting.TotalDue)
|
||||
if (accounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
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";
|
||||
context.MarkDirty();
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
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;
|
||||
context.MarkDirty();
|
||||
@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue)
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
@ -134,14 +136,14 @@ namespace BTCPayServer.HostedServices
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.TotalDue))
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.TotalDue)
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
@ -153,7 +155,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
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"));
|
||||
invoice.Status = "complete";
|
||||
|
@ -14,93 +14,58 @@ using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class RatesHostedService : IHostedService
|
||||
public class RatesHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
CoinAverageSettings coinAverageSettings,
|
||||
IRateProviderFactory rateProviderFactory)
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_coinAverageSettings = coinAverageSettings;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
}
|
||||
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
|
||||
List<Task> _Tasks = new List<Task>();
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
_Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token));
|
||||
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token));
|
||||
return Task.CompletedTask;
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
|
||||
CreateLoopTask(RefreshCoinAverageSettings)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach(var item in tickers
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, caller + " failed");
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
exchanges.Add(item);
|
||||
}
|
||||
}
|
||||
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
|
||||
{
|
||||
return Timer(async () =>
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
_coinAverageSettings.AvailableExchanges = tickers
|
||||
.Exchanges
|
||||
.Select(c => (c.DisplayName, c.Name))
|
||||
.ToArray();
|
||||
await Task.Delay(TimeSpan.FromHours(5), cancellation);
|
||||
}, cancellation);
|
||||
_coinAverageSettings.AvailableExchanges = exchanges;
|
||||
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
||||
}
|
||||
|
||||
Task RefreshCoinAverageSettings(CancellationToken cancellation)
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
return Timer(async () =>
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(cancellation);
|
||||
}, cancellation);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks.ToArray());
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,55 +38,13 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
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)
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||
@ -110,7 +68,6 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -138,7 +95,6 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<CssThemeManager>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
@ -148,6 +104,9 @@ namespace BTCPayServer.Hosting
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
@ -158,6 +117,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
@ -167,30 +128,17 @@ namespace BTCPayServer.Hosting
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
|
||||
services.TryAddSingleton<BTCPayRateProviderFactory>();
|
||||
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
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
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
|
||||
services.AddBundles();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
|
@ -6,37 +6,25 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using System.IO;
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Security.Principal;
|
||||
using NBitpayClient.Extensions;
|
||||
using BTCPayServer.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Models;
|
||||
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 BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class BTCPayMiddleware
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
RequestDelegate _Next;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
TokenRepository tokenRepo,
|
||||
BTCPayServerOptions options)
|
||||
{
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
@ -45,42 +33,16 @@ namespace BTCPayServer.Hosting
|
||||
public async Task Invoke(HttpContext 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();
|
||||
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(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 bitid = new BitIdentity(key);
|
||||
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
|
||||
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
if (!(httpContext.User.Identity is BitIdentity))
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
|
||||
httpContext.SetIsBitpayAPI(isBitpayAPI);
|
||||
if (isBitpayAPI)
|
||||
{
|
||||
httpContext.SetBitpayAuth(bitpayAuth);
|
||||
}
|
||||
await _Next(httpContext);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
@ -100,6 +62,55 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
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 path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
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)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
@ -132,7 +143,7 @@ namespace BTCPayServer.Hosting
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
|
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
@ -0,0 +1,553 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180429083930_legacyapikey")]
|
||||
partial class legacyapikey
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class legacyapikey : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiKeys",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(maxLength: 50, nullable: false),
|
||||
StoreId = table.Column<string>(maxLength: 50, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiKeys", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiKeys_StoreId",
|
||||
table: "ApiKeys",
|
||||
column: "StoreId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiKeys");
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -36,6 +36,22 @@ namespace BTCPayServer.Migrations
|
||||
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")
|
||||
|
@ -19,5 +19,6 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ButtonClass { get; set; } = "btn-danger";
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string Address { get; internal set; }
|
||||
public string Rate { get; internal set; }
|
||||
public string PaymentUrl { get; internal set; }
|
||||
public string Overpaid { get; set; }
|
||||
}
|
||||
public class AddressModel
|
||||
{
|
||||
|
@ -49,6 +49,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool ShowCheckout { get; set; }
|
||||
public string ExceptionStatus { get; set; }
|
||||
public string AmountCurrency
|
||||
{
|
||||
get; set;
|
||||
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
public string HtmlTitle { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string DefaultLang { get; set; }
|
||||
|
@ -18,8 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
|
||||
[EmailAddress]
|
||||
public string TestEmail
|
||||
{
|
||||
|
@ -48,6 +48,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Url]
|
||||
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)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
|
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class RatesViewModel
|
||||
{
|
||||
public class TestResultViewModel
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
public string Rule { get; set; }
|
||||
public bool Error { get; set; }
|
||||
}
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
}
|
||||
|
||||
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
|
||||
public bool ShowScripting { get; set; }
|
||||
|
||||
[Display(Name = "Rate rules")]
|
||||
[MaxLength(2000)]
|
||||
public string Script { get; set; }
|
||||
public string DefaultScript { get; set; }
|
||||
public string ScriptTest { get; set; }
|
||||
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Multiply the rate by ...")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System;
|
||||
@ -12,11 +13,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public class DerivationScheme
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
@ -49,36 +45,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange)
|
||||
{
|
||||
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")]
|
||||
[Range(1, 60 * 24 * 24)]
|
||||
public int InvoiceExpiration
|
||||
@ -119,5 +85,13 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
} = new List<LightningNode>();
|
||||
|
||||
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
|
||||
[Range(0, 100)]
|
||||
public double PaymentTolerance
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "API Key")]
|
||||
public string ApiKey { get; set; }
|
||||
public string EncodedApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
104
BTCPayServer/Rating/CurrencyPair.cs
Normal file
104
BTCPayServer/Rating/CurrencyPair.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public class CurrencyPair
|
||||
{
|
||||
static readonly BTCPayNetworkProvider _NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
|
||||
public CurrencyPair(string left, string right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
Right = right.ToUpperInvariant();
|
||||
Left = left.ToUpperInvariant();
|
||||
}
|
||||
public string Left { get; private set; }
|
||||
public string Right { get; private set; }
|
||||
|
||||
public static CurrencyPair Parse(string str)
|
||||
{
|
||||
if (!TryParse(str, out var result))
|
||||
throw new FormatException("Invalid currency pair");
|
||||
return result;
|
||||
}
|
||||
public static bool TryParse(string str, out CurrencyPair value)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
value = null;
|
||||
str = str.Trim();
|
||||
if (str.Length > 12)
|
||||
return false;
|
||||
var splitted = str.Split(new[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length == 2)
|
||||
{
|
||||
value = new CurrencyPair(splitted[0], splitted[1]);
|
||||
return true;
|
||||
}
|
||||
else if (splitted.Length == 1)
|
||||
{
|
||||
var currencyPair = splitted[0];
|
||||
if (currencyPair.Length < 6 || currencyPair.Length > 10)
|
||||
return false;
|
||||
if (currencyPair.Length == 6)
|
||||
{
|
||||
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
|
||||
return true;
|
||||
}
|
||||
for (int i = 3; i < 5; i++)
|
||||
{
|
||||
var potentialCryptoName = currencyPair.Substring(0, i);
|
||||
var network = _NetworkProvider.GetNetwork(potentialCryptoName);
|
||||
if (network != null)
|
||||
{
|
||||
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
CurrencyPair item = obj as CurrencyPair;
|
||||
if (item == null)
|
||||
return false;
|
||||
return ToString().Equals(item.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool operator ==(CurrencyPair a, CurrencyPair b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.ToString() == b.ToString();
|
||||
}
|
||||
|
||||
public static bool operator !=(CurrencyPair a, CurrencyPair b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ToString().GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Left}_{Right}";
|
||||
}
|
||||
|
||||
public CurrencyPair Inverse()
|
||||
{
|
||||
return new CurrencyPair(Right, Left);
|
||||
}
|
||||
}
|
||||
}
|
96
BTCPayServer/Rating/ExchangeRates.cs
Normal file
96
BTCPayServer/Rating/ExchangeRates.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public class ExchangeRates : IEnumerable<ExchangeRate>
|
||||
{
|
||||
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
|
||||
public ExchangeRates()
|
||||
{
|
||||
|
||||
}
|
||||
public ExchangeRates(IEnumerable<ExchangeRate> rates)
|
||||
{
|
||||
foreach (var rate in rates)
|
||||
{
|
||||
Add(rate);
|
||||
}
|
||||
}
|
||||
List<ExchangeRate> _Rates = new List<ExchangeRate>();
|
||||
public MultiValueDictionary<string, ExchangeRate> ByExchange
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new MultiValueDictionary<string, ExchangeRate>();
|
||||
|
||||
public void Add(ExchangeRate rate)
|
||||
{
|
||||
// 1 DOGE is always 1 DOGE
|
||||
if (rate.CurrencyPair.Left == rate.CurrencyPair.Right)
|
||||
return;
|
||||
var key = $"({rate.Exchange}) {rate.CurrencyPair}";
|
||||
if (_AllRates.TryAdd(key, rate))
|
||||
{
|
||||
_Rates.Add(rate);
|
||||
ByExchange.Add(rate.Exchange, rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rate.Value.HasValue)
|
||||
{
|
||||
_AllRates[key].Value = rate.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<ExchangeRate> GetEnumerator()
|
||||
{
|
||||
return _Rates.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
|
||||
{
|
||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||
{
|
||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||
if (rate != null)
|
||||
rate.Value = value;
|
||||
}
|
||||
}
|
||||
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
|
||||
{
|
||||
if (currencyPair.Left == currencyPair.Right)
|
||||
return 1.0m;
|
||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||
{
|
||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||
if (rate != null)
|
||||
return rate.Value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public class ExchangeRate
|
||||
{
|
||||
public string Exchange { get; set; }
|
||||
public CurrencyPair CurrencyPair { get; set; }
|
||||
public decimal? Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Value == null)
|
||||
return $"{Exchange}({CurrencyPair})";
|
||||
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
}
|
503
BTCPayServer/Rating/RateRules.cs
Normal file
503
BTCPayServer/Rating/RateRules.cs
Normal file
@ -0,0 +1,503 @@
|
||||
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;
|
||||
public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair)
|
||||
{
|
||||
Context.Push(pair);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public ExchangeRates ExchangeRates = new ExchangeRates();
|
||||
public Stack<CurrencyPair> Context { get; set; } = new Stack<CurrencyPair>();
|
||||
bool IsInvocation;
|
||||
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||
{
|
||||
IsInvocation = true;
|
||||
_ExchangeName = node.Expression.ToString();
|
||||
var result = base.VisitInvocationExpression(node);
|
||||
IsInvocation = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
string _ExchangeName = null;
|
||||
|
||||
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||
const int MaxNestedCount = 8;
|
||||
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
|
||||
{
|
||||
var ctx = Context.Peek();
|
||||
|
||||
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? ctx.Left : currentPair.Left,
|
||||
right: currentPair.Right == "X" ? ctx.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 (Context.Count > MaxNestedCount)
|
||||
{
|
||||
Errors.Add(RateRulesErrors.TooMuchNestedCalls);
|
||||
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||
}
|
||||
Context.Push(replacedPair);
|
||||
var replaced = Visit(bestCandidate);
|
||||
if (replaced is ExpressionSyntax expression)
|
||||
{
|
||||
var hasBinaryOps = new HasBinaryOperations();
|
||||
hasBinaryOps.Visit(expression);
|
||||
if (hasBinaryOps.Result)
|
||||
{
|
||||
replaced = SyntaxFactory.ParenthesizedExpression(expression);
|
||||
}
|
||||
}
|
||||
Context.Pop();
|
||||
if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls))
|
||||
{
|
||||
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||
}
|
||||
return replaced;
|
||||
}
|
||||
}
|
||||
return base.VisitIdentifierName(node);
|
||||
}
|
||||
}
|
||||
private SyntaxNode expression;
|
||||
FlattenExpressionRewriter flatten;
|
||||
|
||||
public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate)
|
||||
{
|
||||
flatten = new FlattenExpressionRewriter(parent, currencyPair);
|
||||
this.expression = flatten.Visit(candidate);
|
||||
}
|
||||
|
||||
public ExchangeRates ExchangeRates
|
||||
{
|
||||
get
|
||||
{
|
||||
return flatten.ExchangeRates;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool Reevaluate()
|
||||
{
|
||||
_Value = null;
|
||||
_EvaluatedNode = null;
|
||||
_Evaluated = null;
|
||||
Errors.Clear();
|
||||
|
||||
var rewriter = new ReplaceExchangeRateRewriter();
|
||||
rewriter.Rates = ExchangeRates;
|
||||
var result = rewriter.Visit(this.expression);
|
||||
Errors.AddRange(rewriter.Errors);
|
||||
_Evaluated = result.NormalizeWhitespace("", "\n").ToString();
|
||||
if (HasError)
|
||||
return false;
|
||||
|
||||
var calculate = new CalculateWalker();
|
||||
calculate.Visit(result);
|
||||
if (calculate.Values.Count != 1 || calculate.Errors.Count != 0)
|
||||
{
|
||||
Errors.AddRange(calculate.Errors);
|
||||
return false;
|
||||
}
|
||||
_Value = calculate.Values.Pop();
|
||||
_EvaluatedNode = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private readonly HashSet<RateRulesErrors> _Errors = new HashSet<RateRulesErrors>();
|
||||
public HashSet<RateRulesErrors> Errors
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Errors;
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxNode _EvaluatedNode;
|
||||
string _Evaluated;
|
||||
public bool HasError
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Errors.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string ToString(bool evaluated)
|
||||
{
|
||||
if (!evaluated)
|
||||
return ToString();
|
||||
if (_Evaluated == null)
|
||||
return "Call Evaluate() first";
|
||||
return _Evaluated;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return expression.NormalizeWhitespace("", "\n").ToString();
|
||||
}
|
||||
|
||||
decimal? _Value;
|
||||
public decimal? Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
BTCPayServer/Security/BTCPayClaimsFilter.cs
Normal file
66
BTCPayServer/Security/BTCPayClaimsFilter.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
|
||||
public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
StoreRepository _StoreRepository;
|
||||
public BTCPayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||
{
|
||||
options.Filters.Add(typeof(BTCPayClaimsFilter));
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (!context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var identity = ((ClaimsIdentity)principal.Identity);
|
||||
if (principal.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
|
||||
}
|
||||
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
|
||||
{
|
||||
var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
|
||||
if (claim != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, claim.Value);
|
||||
if (store == null)
|
||||
context.Result = new ChallengeResult(Policies.CookieAuthentication);
|
||||
else
|
||||
{
|
||||
context.HttpContext.SetStoreData(store);
|
||||
if (store != null)
|
||||
{
|
||||
identity.AddClaims(store.GetClaims());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
196
BTCPayServer/Security/BitpayClaimsFilter.cs
Normal file
196
BTCPayServer/Security/BitpayClaimsFilter.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBitpayClient.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
StoreRepository _StoreRepository;
|
||||
TokenRepository _TokenRepository;
|
||||
|
||||
public BitpayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_TokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||
{
|
||||
options.Filters.Add(typeof(BitpayClaimsFilter));
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var bitpayAuth = context.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
var failedAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
|
||||
{
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
context.HttpContext.SetStoreData(store);
|
||||
}
|
||||
else if (failedAuth)
|
||||
{
|
||||
throw new BitpayHttpException(401, "Can't access to store");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string storeId = null;
|
||||
string body = string.Empty;
|
||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = httpContext.Request.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var sin = key.GetBitIDSIN();
|
||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.SIN, sin));
|
||||
|
||||
string token = null;
|
||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||
{
|
||||
token = tokenValues[0];
|
||||
}
|
||||
|
||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||
{
|
||||
try
|
||||
{
|
||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
if (bitToken == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
storeId = bitToken.StoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
return storeId;
|
||||
}
|
||||
|
||||
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||
{
|
||||
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string apiKey = null;
|
||||
try
|
||||
{
|
||||
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/Security/Policies.cs
Normal file
38
BTCPayServer/Security/Policies.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public static class Policies
|
||||
{
|
||||
public const string CookieAuthentication = "Identity.Application";
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
AddClaim(options, CanUseStore.Key);
|
||||
AddClaim(options, CanModifyStoreSettings.Key);
|
||||
AddClaim(options, CanModifyServerSettings.Key);
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void AddClaim(AuthorizationOptions options, string key)
|
||||
{
|
||||
options.AddPolicy(key, o => o.RequireClaim(key));
|
||||
}
|
||||
|
||||
public class CanModifyServerSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifyserversettings";
|
||||
}
|
||||
public class CanUseStore
|
||||
{
|
||||
public const string Key = "btcpay.store.canusestore";
|
||||
}
|
||||
public class CanModifyStoreSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||
}
|
||||
}
|
||||
}
|
15
BTCPayServer/Services/Claims.cs
Normal file
15
BTCPayServer/Services/Claims.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Claims
|
||||
{
|
||||
public const string SIN = "BITID_SIN";
|
||||
|
||||
public const string OwnStore = "BTCPAY_OWN_STORE";
|
||||
}
|
||||
}
|
@ -39,6 +39,8 @@ namespace BTCPayServer.Services.Fees
|
||||
ExplorerClient _ExplorerClient;
|
||||
public async Task<FeeRate> GetFeeRateAsync()
|
||||
{
|
||||
if (!_ExplorerClient.Network.SupportEstimatesSmartFee)
|
||||
return _Factory.Fallback;
|
||||
try
|
||||
{
|
||||
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
|
||||
|
@ -314,6 +314,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
public bool ExtendedNotifications { get; set; }
|
||||
public List<InvoiceEventData> Events { get; internal set; }
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
@ -337,15 +338,16 @@ namespace BTCPayServer.Services.Invoices
|
||||
Flags = new Flags() { Refundable = Refundable }
|
||||
};
|
||||
|
||||
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
|
||||
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
|
||||
foreach (var info in this.GetPaymentMethods(networkProvider, true))
|
||||
foreach (var info in this.GetPaymentMethods(networkProvider))
|
||||
{
|
||||
var accounting = info.Calculate();
|
||||
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
|
||||
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
|
||||
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
|
||||
cryptoInfo.Rate = info.Rate;
|
||||
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
|
||||
cryptoInfo.Price = (accounting.TotalDue - accounting.NetworkFee).ToString();
|
||||
|
||||
cryptoInfo.Due = accounting.Due.ToString();
|
||||
cryptoInfo.Paid = accounting.Paid.ToString();
|
||||
@ -359,14 +361,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
|
||||
};
|
||||
|
||||
var paymentId = info.GetId();
|
||||
var scheme = info.Network.UriScheme;
|
||||
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
|
||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
|
||||
|
||||
|
||||
if (info.GetId().PaymentType == PaymentTypes.BTCLike)
|
||||
if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
@ -375,7 +376,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
}
|
||||
var paymentId = info.GetId();
|
||||
|
||||
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
@ -386,7 +387,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning disable CS0618
|
||||
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
dto.Url = cryptoInfo.Url;
|
||||
dto.BTCPrice = cryptoInfo.Price;
|
||||
dto.Rate = cryptoInfo.Rate;
|
||||
dto.ExRates = cryptoInfo.ExRates;
|
||||
@ -396,8 +396,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
dto.PaymentUrls = cryptoInfo.PaymentUrls;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
if (!info.IsPhantomBTC)
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
}
|
||||
|
||||
Populate(ProductInformation, dto);
|
||||
@ -405,7 +404,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||
dto.Guid = Guid.NewGuid().ToString();
|
||||
|
||||
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
|
||||
return dto;
|
||||
}
|
||||
@ -432,26 +430,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
|
||||
}
|
||||
|
||||
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false)
|
||||
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
|
||||
var serializer = new Serializer(Dummy);
|
||||
PaymentMethod phantom = null;
|
||||
#pragma warning disable CS0618
|
||||
// Legacy
|
||||
if (alwaysIncludeBTC)
|
||||
{
|
||||
var btcNetwork = networkProvider?.GetNetwork("BTC");
|
||||
phantom = new PaymentMethod() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork };
|
||||
if (btcNetwork != null || networkProvider == null)
|
||||
rates.Add(phantom);
|
||||
}
|
||||
if (PaymentMethod != null)
|
||||
{
|
||||
foreach (var prop in PaymentMethod.Properties())
|
||||
{
|
||||
if (prop.Name == "BTC" && phantom != null)
|
||||
rates.Remove(phantom);
|
||||
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
|
||||
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
|
||||
r.CryptoCode = paymentMethodId.CryptoCode;
|
||||
@ -537,6 +524,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
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
|
||||
@ -635,20 +626,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
||||
public string DepositAddress { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsPhantomBTC { get; set; }
|
||||
|
||||
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null)
|
||||
{
|
||||
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
|
||||
var paymentMethods = ParentEntity.GetPaymentMethods(null, IsPhantomBTC);
|
||||
var paymentMethods = ParentEntity.GetPaymentMethods(null);
|
||||
|
||||
var totalDue = ParentEntity.ProductInformation.Price / Rate;
|
||||
var paid = 0m;
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
int precision = 8;
|
||||
var paidTxFee = 0m;
|
||||
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||
int txRequired = 0;
|
||||
var payments =
|
||||
@ -662,9 +650,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (!paidEnough)
|
||||
{
|
||||
totalDue += txFee;
|
||||
paidTxFee += txFee;
|
||||
}
|
||||
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
|
||||
paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision);
|
||||
if (GetId() == _.GetPaymentMethodId())
|
||||
{
|
||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||
@ -680,16 +667,16 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
txRequired++;
|
||||
totalDue += GetTxFee();
|
||||
paidTxFee += GetTxFee();
|
||||
}
|
||||
|
||||
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||
accounting.Paid = Money.Coins(paid);
|
||||
accounting.Paid = Money.Coins(Extensions.RoundUp(paid, precision));
|
||||
accounting.TxRequired = txRequired;
|
||||
accounting.CryptoPaid = Money.Coins(cryptoPaid);
|
||||
accounting.CryptoPaid = Money.Coins(Extensions.RoundUp(cryptoPaid, precision));
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||
accounting.NetworkFee = Money.Coins(paidTxFee);
|
||||
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||
accounting.MinimumTotalDue = Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)));
|
||||
return accounting;
|
||||
}
|
||||
|
||||
@ -762,7 +749,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
paymentData.Outpoint = Outpoint;
|
||||
return paymentData;
|
||||
}
|
||||
if(GetPaymentMethodId().PaymentType== PaymentTypes.LightningLike)
|
||||
if (GetPaymentMethodId().PaymentType == PaymentTypes.LightningLike)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
|
||||
}
|
||||
|
@ -436,6 +436,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => statusSet.Contains(i.Status));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (queryObject.Skip != null)
|
||||
@ -451,6 +457,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)
|
||||
{
|
||||
if (outputs.Length == 0)
|
||||
@ -618,6 +647,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string[] ExceptionStatus
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceId
|
||||
{
|
||||
get;
|
||||
|
@ -31,6 +31,7 @@ namespace BTCPayServer.Services
|
||||
new Language("nl-NL", "Dutch"),
|
||||
new Language("cs-CZ", "Česky"),
|
||||
new Language("is-IS", "Íslenska"),
|
||||
new Language("hr-HR", "Croatian"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ namespace BTCPayServer.Services.Mails
|
||||
}
|
||||
public async Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
||||
if (settings == null)
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
if (!settings.IsComplete())
|
||||
{
|
||||
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
||||
return;
|
||||
@ -36,8 +36,8 @@ namespace BTCPayServer.Services.Mails
|
||||
|
||||
public async Task SendMailCore(string email, string subject, string message)
|
||||
{
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
||||
if (settings == null)
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
if (!settings.IsComplete())
|
||||
throw new InvalidOperationException("Email settings not configured");
|
||||
var smtp = settings.CreateSmtpClient();
|
||||
MailMessage mail = new MailMessage(settings.From, email, subject, message);
|
||||
|
@ -10,30 +10,25 @@ namespace BTCPayServer.Services.Mails
|
||||
{
|
||||
public class EmailSettings
|
||||
{
|
||||
[Required]
|
||||
public string Server
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public int? Port
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public String Login
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public String Password
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[EmailAddress]
|
||||
public string From
|
||||
{
|
||||
@ -45,6 +40,18 @@ namespace BTCPayServer.Services.Mails
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool IsComplete()
|
||||
{
|
||||
SmtpClient smtp = null;
|
||||
try
|
||||
{
|
||||
smtp = CreateSmtpClient();
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
public SmtpClient CreateSmtpClient()
|
||||
{
|
||||
SmtpClient client = new SmtpClient(Server, Port.Value);
|
||||
|
@ -3,13 +3,36 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BTCPayRateProviderFactory : IRateProviderFactory
|
||||
public class ExchangeException
|
||||
{
|
||||
public Exception Exception { get; set; }
|
||||
public string ExchangeName { get; set; }
|
||||
}
|
||||
public class RateResult
|
||||
{
|
||||
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
|
||||
public string Rule { get; set; }
|
||||
public string EvaluatedRule { get; set; }
|
||||
public HashSet<RateRulesErrors> Errors { get; set; }
|
||||
public decimal? Value { get; set; }
|
||||
public bool Cached { get; internal set; }
|
||||
}
|
||||
|
||||
public class BTCPayRateProviderFactory
|
||||
{
|
||||
class QueryRateResult
|
||||
{
|
||||
public bool CachedResult { get; set; }
|
||||
public List<ExchangeException> Exceptions { get; set; }
|
||||
public ExchangeRates ExchangeRates { get; set; }
|
||||
}
|
||||
IMemoryCache _Cache;
|
||||
private IOptions<MemoryCacheOptions> _CacheOptions;
|
||||
|
||||
@ -20,18 +43,57 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Cache;
|
||||
}
|
||||
}
|
||||
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, IServiceProvider serviceProvider)
|
||||
CoinAverageSettings _CoinAverageSettings;
|
||||
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
|
||||
BTCPayNetworkProvider btcpayNetworkProvider,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
if (cacheOptions == null)
|
||||
throw new ArgumentNullException(nameof(cacheOptions));
|
||||
_CoinAverageSettings = coinAverageSettings;
|
||||
_Cache = new MemoryCache(cacheOptions);
|
||||
_CacheOptions = cacheOptions;
|
||||
// We use 15 min because of limits with free version of bitcoinaverage
|
||||
CacheSpan = TimeSpan.FromMinutes(15.0);
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.btcpayNetworkProvider = btcpayNetworkProvider;
|
||||
InitExchanges();
|
||||
}
|
||||
|
||||
IServiceProvider serviceProvider;
|
||||
public bool UseCoinAverageAsFallback { get; set; } = true;
|
||||
|
||||
private void InitExchanges()
|
||||
{
|
||||
// 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));
|
||||
|
||||
// Handmade providers
|
||||
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
|
||||
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
|
||||
// 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()));
|
||||
}
|
||||
|
||||
|
||||
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
|
||||
public Dictionary<string, IRateProvider> DirectProviders
|
||||
{
|
||||
get
|
||||
{
|
||||
return _DirectProviders;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BTCPayNetworkProvider btcpayNetworkProvider;
|
||||
TimeSpan _CacheSpan;
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
@ -51,45 +113,87 @@ namespace BTCPayServer.Services.Rates
|
||||
_Cache = new MemoryCache(_CacheOptions);
|
||||
}
|
||||
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
|
||||
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
|
||||
{
|
||||
rules = rules ?? new RateRules();
|
||||
var rateProvider = GetDefaultRateProvider(network);
|
||||
if (!rules.PreferredExchange.IsCoinAverage())
|
||||
{
|
||||
rateProvider = CreateExchangeRateProvider(network, rules.PreferredExchange);
|
||||
}
|
||||
rateProvider = CreateCachedRateProvider(network, rateProvider, rules.PreferredExchange);
|
||||
return new TweakRateProvider(network, rateProvider, rules);
|
||||
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
|
||||
}
|
||||
|
||||
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
|
||||
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
|
||||
{
|
||||
if (rules == null)
|
||||
throw new ArgumentNullException(nameof(rules));
|
||||
|
||||
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
|
||||
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
|
||||
var consolidatedRates = new ExchangeRates();
|
||||
|
||||
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
|
||||
{
|
||||
var dependentQueries = new List<Task<QueryRateResult>>();
|
||||
foreach (var requiredExchange in i.RateRule.ExchangeRates)
|
||||
{
|
||||
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
|
||||
{
|
||||
fetching = QueryRates(requiredExchange.Exchange);
|
||||
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
|
||||
}
|
||||
dependentQueries.Add(fetching);
|
||||
}
|
||||
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
|
||||
}
|
||||
return fetchingRates;
|
||||
}
|
||||
|
||||
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
|
||||
{
|
||||
var result = new RateResult();
|
||||
result.Cached = true;
|
||||
foreach (var queryAsync in dependentQueries)
|
||||
{
|
||||
var query = await queryAsync;
|
||||
if (!query.CachedResult)
|
||||
result.Cached = false;
|
||||
result.ExchangeExceptions.AddRange(query.Exceptions);
|
||||
foreach (var rule in query.ExchangeRates)
|
||||
{
|
||||
rateRule.ExchangeRates.Add(rule);
|
||||
}
|
||||
}
|
||||
rateRule.Reevaluate();
|
||||
result.Value = rateRule.Value;
|
||||
result.Errors = rateRule.Errors;
|
||||
result.EvaluatedRule = rateRule.ToString(true);
|
||||
result.Rule = rateRule.ToString(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private async Task<QueryRateResult> QueryRates(string exchangeName)
|
||||
{
|
||||
List<IRateProvider> providers = new List<IRateProvider>();
|
||||
|
||||
if(exchange == "quadrigacx")
|
||||
if (DirectProviders.TryGetValue(exchangeName, out var directProvider))
|
||||
providers.Add(directProvider);
|
||||
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
|
||||
{
|
||||
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
|
||||
providers.Add(new CoinAverageRateProvider()
|
||||
{
|
||||
Exchange = exchangeName,
|
||||
Authenticator = _CoinAverageSettings
|
||||
});
|
||||
}
|
||||
|
||||
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
|
||||
coinAverage.Exchange = exchange;
|
||||
providers.Add(coinAverage);
|
||||
return new FallbackRateProvider(providers.ToArray());
|
||||
}
|
||||
|
||||
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
|
||||
{
|
||||
return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope };
|
||||
}
|
||||
|
||||
private IRateProvider GetDefaultRateProvider(BTCPayNetwork network)
|
||||
{
|
||||
if(network.DefaultRateProvider == null)
|
||||
var fallback = new FallbackRateProvider(providers.ToArray());
|
||||
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
|
||||
{
|
||||
throw new RateUnavailableException(network.CryptoCode);
|
||||
}
|
||||
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
|
||||
CacheSpan = CacheSpan
|
||||
};
|
||||
var value = await cached.GetRatesAsync();
|
||||
return new QueryRateResult()
|
||||
{
|
||||
CachedResult = !fallback.Used,
|
||||
ExchangeRates = value,
|
||||
Exceptions = fallback.Exceptions
|
||||
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,18 +5,13 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BitpayRateProviderDescription : RateProviderDescription
|
||||
{
|
||||
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
}
|
||||
}
|
||||
public class BitpayRateProvider : IRateProvider
|
||||
{
|
||||
public const string BitpayName = "bitpay";
|
||||
Bitpay _Bitpay;
|
||||
public BitpayRateProvider(Bitpay bitpay)
|
||||
{
|
||||
@ -24,21 +19,13 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new ArgumentNullException(nameof(bitpay));
|
||||
_Bitpay = bitpay;
|
||||
}
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false);
|
||||
var rate = rates.GetRate(currency);
|
||||
if (rate == 0m)
|
||||
throw new RateUnavailableException(currency);
|
||||
return (decimal)rate;
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return (await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
.AllRates
|
||||
.Select(r => new Rate() { Currency = r.Code, Value = r.Value })
|
||||
.ToList();
|
||||
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
|
||||
.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
@ -10,9 +11,8 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
private IRateProvider _Inner;
|
||||
private IMemoryCache _MemoryCache;
|
||||
private string _CryptoCode;
|
||||
|
||||
public CachedRateProvider(string cryptoCode, IRateProvider inner, IMemoryCache memoryCache)
|
||||
public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache)
|
||||
{
|
||||
if (inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new ArgumentNullException(nameof(memoryCache));
|
||||
this._Inner = inner;
|
||||
this.MemoryCache = memoryCache;
|
||||
this._CryptoCode = cryptoCode;
|
||||
this.ExchangeName = exchangeName;
|
||||
}
|
||||
|
||||
public IRateProvider Inner
|
||||
@ -31,31 +31,22 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public string ExchangeName { get; set; }
|
||||
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
|
||||
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRateAsync(currency);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public string AdditionalScope { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -21,29 +22,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public class CoinAverageRateProviderDescription : RateProviderDescription
|
||||
{
|
||||
public CoinAverageRateProviderDescription(string crypto)
|
||||
{
|
||||
CryptoCode = crypto;
|
||||
}
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new CoinAverageRateProvider(CryptoCode)
|
||||
{
|
||||
Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>()
|
||||
};
|
||||
}
|
||||
|
||||
IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return CreateRateProvider(serviceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetExchangeTickersResponse
|
||||
{
|
||||
public class Exchange
|
||||
@ -69,18 +47,18 @@ namespace BTCPayServer.Services.Rates
|
||||
public interface ICoinAverageAuthenticator
|
||||
{
|
||||
Task AddHeader(HttpRequestMessage message);
|
||||
}
|
||||
}
|
||||
|
||||
public class CoinAverageRateProvider : IRateProvider
|
||||
{
|
||||
public const string CoinAverageName = "coinaverage";
|
||||
public CoinAverageRateProvider()
|
||||
{
|
||||
|
||||
}
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
public CoinAverageRateProvider(string cryptoCode)
|
||||
{
|
||||
CryptoCode = cryptoCode ?? "BTC";
|
||||
}
|
||||
|
||||
public string Exchange { get; set; }
|
||||
public string Exchange { get; set; } = CoinAverageName;
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
@ -88,27 +66,19 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
get; set;
|
||||
} = "global";
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rates = await GetRatesCore();
|
||||
return GetRate(rates, currency);
|
||||
}
|
||||
|
||||
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
|
||||
{
|
||||
if (currency == "BTC")
|
||||
return 1.0m;
|
||||
if (rates.TryGetValue(currency, out decimal result))
|
||||
return result;
|
||||
throw new RateUnavailableException(currency);
|
||||
}
|
||||
|
||||
public ICoinAverageAuthenticator Authenticator { get; set; }
|
||||
|
||||
private async Task<Dictionary<string, decimal>> GetRatesCore()
|
||||
private bool TryToDecimal(JProperty p, out decimal v)
|
||||
{
|
||||
string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"];
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
var auth = Authenticator;
|
||||
@ -128,36 +98,29 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
if(Exchange != null)
|
||||
if (Exchange != CoinAverageName)
|
||||
{
|
||||
rates = (JObject)rates["symbols"];
|
||||
}
|
||||
return rates.Properties()
|
||||
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused))
|
||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
|
||||
{
|
||||
TryToDecimal(p, out decimal v);
|
||||
return v;
|
||||
});
|
||||
|
||||
var exchangeRates = new ExchangeRates();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
ExchangeRate exchangeRate = new ExchangeRate();
|
||||
exchangeRate.Exchange = Exchange;
|
||||
if (!TryToDecimal(prop, out decimal value))
|
||||
continue;
|
||||
exchangeRate.Value = value;
|
||||
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()
|
||||
{
|
||||
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"];
|
||||
response.Exchanges = exchanges
|
||||
.Properties()
|
||||
.Select(p =>
|
||||
.Select(p =>
|
||||
{
|
||||
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
|
||||
exchange.Name = p.Name;
|
||||
|
@ -20,12 +20,43 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Settings.AddHeader(message);
|
||||
}
|
||||
}
|
||||
|
||||
public class CoinAverageExchange
|
||||
{
|
||||
public CoinAverageExchange(string name, string display)
|
||||
{
|
||||
Name = name;
|
||||
Display = display;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Display { get; set; }
|
||||
public 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()
|
||||
{
|
||||
Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
}
|
||||
|
||||
public void Add(CoinAverageExchange exchange)
|
||||
{
|
||||
Add(exchange.Name, exchange);
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
|
||||
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
|
||||
public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges();
|
||||
|
||||
public CoinAverageSettings()
|
||||
{
|
||||
@ -37,8 +68,9 @@ namespace BTCPayServer.Services.Rates
|
||||
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
|
||||
//}
|
||||
//b.AppendLine("}.ToArray()");
|
||||
|
||||
AvailableExchanges = new[] {
|
||||
AvailableExchanges = new CoinAverageExchanges();
|
||||
foreach(var item in
|
||||
new[] {
|
||||
(DisplayName: "BitBargain", Name: "bitbargain"),
|
||||
(DisplayName: "Tidex", Name: "tidex"),
|
||||
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
||||
@ -89,7 +121,10 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Quoine", Name: "quoine"),
|
||||
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
}.ToArray();
|
||||
})
|
||||
{
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
|
74
BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs
Normal file
74
BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ExchangeSharpRateProvider : IRateProvider
|
||||
{
|
||||
readonly ExchangeAPI _ExchangeAPI;
|
||||
readonly string _ExchangeName;
|
||||
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
|
||||
{
|
||||
if (exchangeAPI == null)
|
||||
throw new ArgumentNullException(nameof(exchangeAPI));
|
||||
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
|
||||
_ExchangeAPI = exchangeAPI;
|
||||
_ExchangeName = exchangeName;
|
||||
ReverseCurrencyPair = reverseCurrencyPair;
|
||||
}
|
||||
|
||||
public bool ReverseCurrencyPair
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = await _ExchangeAPI.GetTickersAsync();
|
||||
lock (notFoundSymbols)
|
||||
{
|
||||
var exchangeRates =
|
||||
rates.Select(t => CreateExchangeRate(t))
|
||||
.Where(t => t != null)
|
||||
.ToArray();
|
||||
return new ExchangeRates(exchangeRates);
|
||||
}
|
||||
}
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
HashSet<string> notFoundSymbols = new HashSet<string>();
|
||||
private ExchangeRate CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
|
||||
{
|
||||
if (notFoundSymbols.Contains(ticker.Key))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key);
|
||||
if (!CurrencyPair.TryParse(tickerName, out var pair))
|
||||
{
|
||||
notFoundSymbols.Add(ticker.Key);
|
||||
return null;
|
||||
}
|
||||
if(ReverseCurrencyPair)
|
||||
pair = new CurrencyPair(pair.Right, pair.Left);
|
||||
var rate = new ExchangeRate();
|
||||
rate.CurrencyPair = pair;
|
||||
rate.Exchange = _ExchangeName;
|
||||
rate.Value = ticker.Value.Bid;
|
||||
return rate;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
notFoundSymbols.Add(ticker.Key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,58 +2,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class FallbackRateProviderDescription : RateProviderDescription
|
||||
{
|
||||
public FallbackRateProviderDescription(RateProviderDescription[] rateProviders)
|
||||
{
|
||||
RateProviders = rateProviders;
|
||||
}
|
||||
|
||||
public RateProviderDescription[] RateProviders { get; set; }
|
||||
|
||||
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new FallbackRateProvider(RateProviders.Select(r => r.CreateRateProvider(serviceProvider)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class FallbackRateProvider : IRateProvider
|
||||
{
|
||||
|
||||
IRateProvider[] _Providers;
|
||||
public bool Used { get; set; }
|
||||
public FallbackRateProvider(IRateProvider[] providers)
|
||||
{
|
||||
if (providers == null)
|
||||
throw new ArgumentNullException(nameof(providers));
|
||||
_Providers = providers;
|
||||
}
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
foreach(var p in _Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await p.GetRateAsync(currency).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
throw new RateUnavailableException(currency);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
Used = true;
|
||||
foreach (var p in _Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await p.GetRatesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
catch(Exception ex) { Exceptions.Add(ex); }
|
||||
}
|
||||
throw new RateUnavailableException("ALL");
|
||||
return new ExchangeRates();
|
||||
}
|
||||
|
||||
public List<Exception> Exceptions { get; set; } = new List<Exception>();
|
||||
}
|
||||
}
|
||||
|
@ -2,32 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class Rate
|
||||
{
|
||||
public Rate()
|
||||
{
|
||||
|
||||
}
|
||||
public Rate(string currency, decimal value)
|
||||
{
|
||||
Value = value;
|
||||
Currency = currency;
|
||||
}
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public decimal Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public interface IRateProvider
|
||||
{
|
||||
Task<decimal> GetRateAsync(string currency);
|
||||
Task<ICollection<Rate>> GetRatesAsync();
|
||||
Task<ExchangeRates> GetRatesAsync();
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class RateRules : IEnumerable<RateRule>
|
||||
{
|
||||
private List<RateRule> rateRules;
|
||||
|
||||
public RateRules()
|
||||
{
|
||||
rateRules = new List<RateRule>();
|
||||
}
|
||||
public RateRules(List<RateRule> rateRules)
|
||||
{
|
||||
this.rateRules = rateRules?.ToList() ?? new List<RateRule>();
|
||||
}
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IEnumerator<RateRule> GetEnumerator()
|
||||
{
|
||||
return rateRules.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
public interface IRateProviderFactory
|
||||
{
|
||||
IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules);
|
||||
TimeSpan CacheSpan { get; set; }
|
||||
void InvalidateCache();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class MockRateProviderFactory : IRateProviderFactory
|
||||
{
|
||||
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
|
||||
public MockRateProviderFactory()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TimeSpan CacheSpan { get; set; }
|
||||
|
||||
public void AddMock(MockRateProvider mock)
|
||||
{
|
||||
_Mocks.Add(mock);
|
||||
}
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
|
||||
{
|
||||
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
List<Rate> _Rates;
|
||||
|
||||
public string CryptoCode { get; }
|
||||
|
||||
public MockRateProvider(string cryptoCode, params Rate[] rates)
|
||||
{
|
||||
_Rates = new List<Rate>(rates);
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public MockRateProvider(string cryptoCode, List<Rate> rates)
|
||||
{
|
||||
_Rates = rates;
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase));
|
||||
if (rate == null)
|
||||
throw new RateUnavailableException(currency);
|
||||
return Task.FromResult(rate.Value);
|
||||
}
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
ICollection<Rate> rates = _Rates;
|
||||
return Task.FromResult(rates);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,32 +4,15 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class QuadrigacxRateProvider : IRateProvider
|
||||
{
|
||||
public QuadrigacxRateProvider(string crypto)
|
||||
{
|
||||
CryptoCode = crypto;
|
||||
}
|
||||
public string CryptoCode { get; set; }
|
||||
public const string QuadrigacxName = "quadrigacx";
|
||||
static HttpClient _Client = new HttpClient();
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return await GetRatesAsyncCore(CryptoCode, currency);
|
||||
}
|
||||
|
||||
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
if (!TryToDecimal(rates, out var result))
|
||||
throw new RateUnavailableException(currency);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryToDecimal(JObject p, out decimal v)
|
||||
{
|
||||
@ -40,26 +23,26 @@ namespace BTCPayServer.Services.Rates
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
List<Rate> result = new List<Rate>();
|
||||
var exchangeRates = new ExchangeRates();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
var rate = new Rate();
|
||||
var splitted = prop.Name.Split('_');
|
||||
var crypto = splitted[0].ToUpperInvariant();
|
||||
if (crypto != CryptoCode)
|
||||
var rate = new ExchangeRate();
|
||||
if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
continue;
|
||||
rate.CurrencyPair = pair;
|
||||
rate.Exchange = QuadrigacxName;
|
||||
if (!TryToDecimal((JObject)prop.Value, out var v))
|
||||
continue;
|
||||
rate.Currency = splitted[1].ToUpperInvariant();
|
||||
TryToDecimal((JObject)prop.Value, out var v);
|
||||
rate.Value = v;
|
||||
result.Add(rate);
|
||||
exchangeRates.Add(rate);
|
||||
}
|
||||
return result;
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface RateProviderDescription
|
||||
{
|
||||
IRateProvider CreateRateProvider(IServiceProvider serviceProvider);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class RateUnavailableException : Exception
|
||||
{
|
||||
public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency)
|
||||
{
|
||||
if (currency == null)
|
||||
throw new ArgumentNullException(nameof(currency));
|
||||
Currency = currency;
|
||||
}
|
||||
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class TweakRateProvider : IRateProvider
|
||||
{
|
||||
private BTCPayNetwork network;
|
||||
private IRateProvider rateProvider;
|
||||
private RateRules rateRules;
|
||||
|
||||
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (rateProvider == null)
|
||||
throw new ArgumentNullException(nameof(rateProvider));
|
||||
if (rateRules == null)
|
||||
throw new ArgumentNullException(nameof(rateRules));
|
||||
this.network = network;
|
||||
this.rateProvider = rateProvider;
|
||||
this.rateRules = rateRules;
|
||||
}
|
||||
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rate = await rateProvider.GetRateAsync(currency);
|
||||
foreach(var rule in rateRules)
|
||||
{
|
||||
rate = rule.Apply(network, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
List<Rate> rates = new List<Rate>();
|
||||
foreach (var rate in await rateProvider.GetRatesAsync())
|
||||
{
|
||||
var localRate = rate.Value;
|
||||
foreach (var rule in rateRules)
|
||||
{
|
||||
localRate = rule.Apply(network, localRate);
|
||||
}
|
||||
rates.Add(new Rate(rate.Currency, localRate));
|
||||
}
|
||||
return rates;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ namespace BTCPayServer.Services.Stores
|
||||
|
||||
public async Task<StoreData> FindStore(string storeId)
|
||||
{
|
||||
if (storeId == null)
|
||||
return null;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false);
|
||||
@ -42,7 +44,9 @@ namespace BTCPayServer.Services.Stores
|
||||
}).ToArrayAsync())
|
||||
.Select(us =>
|
||||
{
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
us.Store.Role = us.Role;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
return us.Store;
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
@ -82,7 +86,9 @@ namespace BTCPayServer.Services.Stores
|
||||
.ToArrayAsync())
|
||||
.Select(u =>
|
||||
{
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
u.StoreData.Role = u.Role;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
return u.StoreData;
|
||||
}).ToArray();
|
||||
}
|
||||
|
@ -5,11 +5,6 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class StorePolicies
|
||||
{
|
||||
public const string CanAccessStores = "CanAccessStore";
|
||||
public const string OwnStore = "OwnStore";
|
||||
}
|
||||
public class StoreRoles
|
||||
{
|
||||
public const string Owner = "Owner";
|
||||
|
@ -19,9 +19,11 @@
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
@foreach(var item in Model.Items)
|
||||
@for(int i = 0; i < Model.Items.Length; i++)
|
||||
{
|
||||
<div class="col-sm-4 mb-3">
|
||||
var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
|
||||
var item = Model.Items[i];
|
||||
<div class="@className">
|
||||
<h3>@item.Title</h3>
|
||||
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
|
||||
</div>
|
||||
|
@ -66,9 +66,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-item-order__right">
|
||||
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
|
||||
<div class="single-item-order__right__btc-price" v-if="srvModel.status === 'paid'">
|
||||
<span>{{ srvModel.btcPaid }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
<div class="single-item-order__right__btc-price" v-else>
|
||||
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
|
||||
<div class="single-item-order__right__ex-rate">
|
||||
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
|
||||
</div>
|
||||
|
@ -11,12 +11,12 @@
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>BTCPay Invoice</title>
|
||||
<title>@Model.HtmlTitle</title>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
||||
@ -114,7 +114,8 @@
|
||||
'pt-BR': { translation: locales_pt_br },
|
||||
'nl': { translation: locales_nl },
|
||||
'cs-CZ': { translation: locales_cs },
|
||||
'is-IS': { translation: locales_is }
|
||||
'is-IS': { translation: locales_is },
|
||||
'hr-HR': { translation: locales_hr }
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
.smMaxWidth {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
@@media (min-width: 768px) {
|
||||
.smMaxWidth {
|
||||
max-width: 400px;
|
||||
@ -166,10 +167,14 @@
|
||||
<th class="text-right">Rate</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Due</th>
|
||||
@if(Model.StatusException == "paidOver")
|
||||
{
|
||||
<th class="text-right">Overpaid</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.CryptoPayments)
|
||||
@foreach(var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
@ -177,13 +182,17 @@
|
||||
<td class="text-right">@payment.Rate</td>
|
||||
<td class="text-right">@payment.Paid</td>
|
||||
<td class="text-right">@payment.Due</td>
|
||||
@if(Model.StatusException == "paidOver")
|
||||
{
|
||||
<td class="text-right">@payment.Overpaid</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.OnChainPayments.Count > 0)
|
||||
@if(Model.OnChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@ -198,7 +207,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.OnChainPayments)
|
||||
@foreach(var payment in Model.OnChainPayments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "class='linethrough'" : "";
|
||||
<tr @replaced>
|
||||
@ -217,7 +226,7 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.OffChainPayments.Count > 0)
|
||||
@if(Model.OffChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@ -230,7 +239,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.OffChainPayments)
|
||||
@foreach(var payment in Model.OffChainPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.Crypto</td>
|
||||
@ -253,7 +262,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var address in Model.Addresses)
|
||||
@foreach(var address in Model.Addresses)
|
||||
{
|
||||
var current = address.Current ? "font-weight-bold" : "";
|
||||
<tr>
|
||||
@ -277,7 +286,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var evt in Model.Events)
|
||||
@foreach(var evt in Model.Events)
|
||||
{
|
||||
<tr>
|
||||
<td>@evt.Timestamp</td>
|
||||
|
@ -19,15 +19,16 @@
|
||||
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
|
||||
<div id="help" class="collapse text-left">
|
||||
<p>
|
||||
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
|
||||
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters
|
||||
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.<br />
|
||||
You can also apply filters to your search by searching for <code>filtername:value</code>, here is a list of supported filters
|
||||
</p>
|
||||
<ul>
|
||||
<li><b>storeid:id</b> for filtering a specific store</li>
|
||||
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
|
||||
<li><code>storeid:id</code> for filtering a specific store</li>
|
||||
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
|
||||
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`.
|
||||
If you want two confirmed and complete invoices, duplicate the filter: <code>status:confirmed status:complete</code>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -95,7 +96,7 @@
|
||||
}
|
||||
<td style="text-align:right">@invoice.AmountCurrency</td>
|
||||
<td style="text-align:right">
|
||||
@if(invoice.Status == "new")
|
||||
@if(invoice.ShowCheckout)
|
||||
{
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
|
||||
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
|
@ -22,6 +22,7 @@
|
||||
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.AuthenticatorUri)"></div>
|
||||
<br />
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
|
@ -53,6 +53,7 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="TestEmail"></label>
|
||||
<input asp-for="TestEmail" class="form-control" />
|
||||
<span asp-validation-for="TestEmail" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@model RatesViewModel
|
||||
@model BTCPayServer.Models.ServerViewModels.RatesViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Rates);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<form method="post">
|
||||
<button type="submit" class="btn btn-secondary btn-danger" title="Continue">@Model.Action</button>
|
||||
<button type="submit" class="btn btn-secondary @Model.ButtonClass" title="Continue">@Model.Action</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -18,8 +19,8 @@
|
||||
<title>BTCPay Server</title>
|
||||
|
||||
@* CSS *@
|
||||
<link href="@themeManager.BootstrapUri" rel="stylesheet" />
|
||||
<link href="@themeManager.CreativeStartUri" rel="stylesheet" />
|
||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.CreativeStartUri)" rel="stylesheet" />
|
||||
|
||||
<bundle name="wwwroot/bundles/main-bundle.min.css" />
|
||||
|
||||
|
@ -125,7 +125,7 @@
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
<script type="text/javascript">
|
||||
@Model.ServerUrl.ToJSVariableModel("srvModel");
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model.ServerUrl));
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
|
||||
|
@ -26,6 +26,11 @@
|
||||
<input asp-for="CustomCSS" class="form-control" />
|
||||
<span asp-validation-for="CustomCSS" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="HtmlTitle"></label>
|
||||
<input asp-for="HtmlTitle" class="form-control" />
|
||||
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultCryptoCurrency"></label>
|
||||
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
|
@ -5,32 +5,60 @@
|
||||
ViewData.AddActivePage(StoreNavPages.Tokens);
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<p>You can allow a public key to access the API of this store</p>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
<a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>SIN</th>
|
||||
<th>Facade</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var token in Model.Tokens)
|
||||
{
|
||||
<tr>
|
||||
<td>@token.Label</td>
|
||||
<td>@token.SIN</td>
|
||||
<td>@token.Facade</td>
|
||||
<td>
|
||||
<form asp-action="DeleteToken" method="post">
|
||||
<input type="hidden" name="tokenId" value="@token.Id">
|
||||
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Access token</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p>Authorize a public key to access Bitpay compatible Invoice API (<a href="https://support.bitpay.com/hc/en-us/articles/115003001183-How-do-I-pair-my-client-and-create-a-token-">More information</a>)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>SIN</th>
|
||||
<th>Facade</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var token in Model.Tokens)
|
||||
{
|
||||
<tr>
|
||||
<td>@token.Label</td>
|
||||
<td>@token.SIN</td>
|
||||
<td>@token.Facade</td>
|
||||
<td>
|
||||
<form asp-action="DeleteToken" method="post">
|
||||
<input type="hidden" name="tokenId" value="@token.Id">
|
||||
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Legacy API Keys</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p>Alternatively, you can use the invoice API by including the following HTTP Header in your requests:<br /> <code>Authorization: Basic @Model.EncodedApiKey</code> </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post" asp-action="GenerateAPIKey">
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiKey"></label>
|
||||
<input asp-for="ApiKey" readonly class="form-control" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" role="button">Create new API Key</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
161
BTCPayServer/Views/Stores/Rates.cshtml
Normal file
161
BTCPayServer/Views/Stores/Rates.cshtml
Normal file
@ -0,0 +1,161 @@
|
||||
@model BTCPayServer.Models.StoreViewModels.RatesViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Rates";
|
||||
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Rates);
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
@if(Model.ShowScripting)
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Scripting</h5>
|
||||
<span>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</span>
|
||||
<p class="text-muted">
|
||||
<b>Supported exchanges are</b>:
|
||||
@for(int i = 0; i < Model.AvailableExchanges.Length; i++)
|
||||
{
|
||||
<a href="@Model.AvailableExchanges[i].Url">@Model.AvailableExchanges[i].Name</a><span>@(i == Model.AvailableExchanges.Length - 1 ? "" : ",")</span>
|
||||
}
|
||||
</p>
|
||||
<p><a href="#help" data-toggle="collapse"><b>Click here for more information</b></a></p>
|
||||
</div>
|
||||
}
|
||||
@if(Model.TestRateRules != null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Test results:</h5>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tbody>
|
||||
@foreach(var result in Model.TestRateRules)
|
||||
{
|
||||
<tr>
|
||||
@if(result.Error)
|
||||
{
|
||||
<th class="small"><span class="fa fa-times" style="color:red;"></span> @result.CurrencyPair</th>
|
||||
}
|
||||
else
|
||||
{
|
||||
<th class="small"><span class="fa fa-check" style="color:green;"></span> @result.CurrencyPair</th>
|
||||
}
|
||||
<td class="small">@result.Rule</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if(Model.ShowScripting)
|
||||
{
|
||||
<div id="help" class="collapse text-left">
|
||||
<p>
|
||||
The script language is composed of several rules composed of a currency pair and a mathematic expression.
|
||||
The example below will use <code>gdax</code> for both <code>LTC_USD</code> and <code>BTC_USD</code> pairs.
|
||||
</p>
|
||||
<pre>
|
||||
<code>
|
||||
LTC_USD = gdax(LTC_USD);
|
||||
BTC_USD = gdax(BTC_USD);
|
||||
</code>
|
||||
</pre>
|
||||
<p>However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule <code>X_X</code> which will match any currency pair. The following example will use <code>gdax</code> for getting the rate of any currency pair.</p>
|
||||
<pre>
|
||||
<code>
|
||||
X_X = gdax(X_X);
|
||||
</code>
|
||||
</pre>
|
||||
<p>However, <code>gdax</code> does not support the <code>BTC_CAD</code> pair. For this reason you can add a rule mapping all <code>X_CAD</code> to <code>quadrigacx</code>, a Canadian exchange.</p>
|
||||
<pre>
|
||||
<code>
|
||||
X_CAD = quadrigacx(X_CAD);
|
||||
X_X = gdax(X_X);
|
||||
</code>
|
||||
</pre>
|
||||
<p>A given currency pair match the most specific rule. If two rules are matching and are as specific, the first rule will be chosen.</p>
|
||||
<p>
|
||||
But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most exchange do not have any pair for it. But <code>bittrex</code> has a <code>DOGE_BTC</code> pair. <br />
|
||||
Luckily, the rule engine allow you to reference rules:
|
||||
</p>
|
||||
<pre>
|
||||
<code>
|
||||
DOGE_X = bittrex(DOGE_BTC) * BTC_X
|
||||
X_CAD = quadrigacx(X_CAD);
|
||||
X_X = gdax(X_X);
|
||||
</code>
|
||||
</pre>
|
||||
<p>With <code>DOGE_USD</code> will be expanded to <code>bittrex(DOGE_BTC) * gdax(BTC_USD)</code>. And <code>DOGE_CAD</code> will be expanded to <code>bittrex(DOGE_BTC) * quadrigacx(BTC_CAD)</code>. <br />
|
||||
However, we advise you to write it that way to increase coverage so that <code>DOGE_BTC</code> is also supported:</p>
|
||||
<pre>
|
||||
<code>
|
||||
DOGE_X = DOGE_BTC * BTC_X
|
||||
DOGE_BTC = bittrex(DOGE_BTC)
|
||||
X_CAD = quadrigacx(X_CAD);
|
||||
X_X = gdax(X_X);
|
||||
</code>
|
||||
</pre>
|
||||
<p>It is worth noting that the inverses of those pairs are automatically supported as well.<br />
|
||||
It means that the rule <code>USD_DOGE = 1 / DOGE_USD</code> implicitely exists.</p>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Script"></label>
|
||||
<textarea asp-for="Script" rows="20" cols="80" class="form-control"></textarea>
|
||||
<span asp-validation-for="Script" class="text-danger"></span>
|
||||
<a href="#" onclick="$('#Script').val(defaultScript); return false;">Set to default settings</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a asp-action="ShowRateRules" asp-route-scripting="false">Turn off advanced rate rule scripting</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a asp-action="ShowRateRules" asp-route-scripting="true">Turn on advanced rate rule scripting</a>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="RateMultiplier"></label>
|
||||
<input asp-for="RateMultiplier" class="form-control" />
|
||||
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h5>Testing</h5>
|
||||
<span>Enter currency pairs which you want to test against your rule (eg. <code>DOGE_USD,DOGE_CAD,BTC_CAD,BTC_USD</code>)</span>
|
||||
<div class="input-group">
|
||||
<input placeholder="BTC_USD, BTC_CAD" asp-for="ScriptTest" class="form-control" />
|
||||
<span class="input-group-btn">
|
||||
<button name="command" value="Test" type="submit" class="btn btn-primary" title="Test">
|
||||
<span class="fa fa-vial"></span> Test
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<span asp-validation-for="ScriptTest" class="text-danger"></span>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
<input type="hidden" asp-for="ShowScripting" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script type="text/javascript">var defaultScript = @Html.Raw(Json.Serialize(Model.DefaultScript));</script>
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Stores
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
public static string Index => "Index";
|
||||
public static string Rates => "Rates";
|
||||
public static string Checkout => "Checkout experience";
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
@ -20,6 +21,7 @@ namespace BTCPayServer.Views.Stores
|
||||
|
||||
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
|
@ -34,19 +34,6 @@
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RateMultiplier"></label>
|
||||
<input asp-for="RateMultiplier" class="form-control" />
|
||||
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="InvoiceExpiration"></label>
|
||||
<input asp-for="InvoiceExpiration" class="form-control" />
|
||||
@ -57,6 +44,11 @@
|
||||
<input asp-for="MonitoringExpiration" class="form-control" />
|
||||
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PaymentTolerance"></label>
|
||||
<input asp-for="PaymentTolerance" class="form-control" />
|
||||
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SpeedPolicy"></label>
|
||||
<select asp-for="SpeedPolicy" class="form-control">
|
||||
|
@ -65,7 +65,7 @@
|
||||
@section Scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @StoreNavPages.IndexNavClass(ViewContext)" asp-action="UpdateStore">General settings</a>
|
||||
<a class="nav-link @StoreNavPages.RatesNavClass(ViewContext)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @StoreNavPages.CheckoutNavClass(ViewContext)" asp-action="CheckoutExperience">Checkout experience</a>
|
||||
<a class="nav-link @StoreNavPages.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
|
||||
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
|
||||
|
48
BTCPayServer/wwwroot/checkout/js/langs/hr.js
Normal file
48
BTCPayServer/wwwroot/checkout/js/langs/hr.js
Normal file
@ -0,0 +1,48 @@
|
||||
const locales_hr = {
|
||||
nested: {
|
||||
lang: 'Croatian'
|
||||
},
|
||||
"Awaiting Payment...": "Čekamo uplatu...",
|
||||
"Pay with": "Plati sa",
|
||||
"Contact and Refund Email": "E-mail za kontakt i povrat sredstava",
|
||||
"Contact_Body": "Molimo upišite Vašu e-mail adresu. Kontaktirat ćemo Vas ukoliko bude potrebe.",
|
||||
"Your email": "Vaš e-mail",
|
||||
"Continue": "Dalje",
|
||||
"Please enter a valid email address": "Molimo unesite ispravnu e-mail adresu",
|
||||
"Order Amount": "Količina",
|
||||
"Network Cost": "Trošak mreže",
|
||||
"Already Paid": "Već plaćeno",
|
||||
"Due": "Rok",
|
||||
// Tabs
|
||||
"Scan": "Skeniraj",
|
||||
"Copy": "Kopiraj",
|
||||
"Conversion": "Pretvori",
|
||||
// Scan tab
|
||||
"Open in wallet": "Otvori u novčaniku",
|
||||
// Copy tab
|
||||
"CompletePay_Body": "Kako bi završili uplatu pošaljite {{btcDue}} {{cryptoCode}} na navedenu adresu",
|
||||
"Amount": "Iznos",
|
||||
"Address": "Adresa",
|
||||
"Copied": "Kopirano",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Možete platiti {{btcDue}} {{cryptoCode}} pomoću altcoina koje prodavač ne podržava.",
|
||||
"ConversionTab_BodyDesc": "Ovu usluga pruža treća strana. Vodite računa da nemamo kontroli nad načinom kako će Vam davatelji usluge proslijediti sredstva. Vodite računa da je račun plaćen tek kada su primljena sredstva na {{cryptoCode}} Blockchainu.",
|
||||
"Shapeshift_Button_Text": "Plati s Alt-coinovima",
|
||||
"ConversionTab_Lightning": "Ne postoji treća strana koja bi konvertirala Lightning Network uplate.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "Račun uskoro ističe...",
|
||||
"Invoice expired": "Račun je istekao",
|
||||
"What happened?": "Što se dogodilo",
|
||||
"InvoiceExpired_Body_1": "Račun je istekao i nije više valjan. Račun vrijedi samo {{maxTimeMinutes}} minuta. \
|
||||
Možete se vratiti na {{storeName}}, gdje možete ponovo inicirati plaćanje.",
|
||||
"InvoiceExpired_Body_2": "Ako ste pokušali poslati uplatu, ista nije registrirana na Blockchainu. Nismo još zaprimili Vašu uplatu.",
|
||||
"InvoiceExpired_Body_3": "Ako poslana sredstva na budu potvrđena na Blockchainu, sredstva će biti ponovo dostupna u Vašem novčaniku.",
|
||||
"Invoice ID": "Broj računa",
|
||||
"Order ID": "Broj narudžbe",
|
||||
"Return to StoreName": "Vrati se na {{storeName}}",
|
||||
// Invoice paid
|
||||
"This invoice has been paid": "Račun je plaćen",
|
||||
// Invoice archived
|
||||
"This invoice has been archived": "Račun je arhiviran.",
|
||||
"Archived_Body": "Kontaktirajte dućan za detalje oko narudžbe ili pomoć."
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user