Compare commits

...

71 Commits

Author SHA1 Message Date
272ac49872 try to better respect event ordering 2018-05-06 02:06:07 +09:00
5f05ca5ac6 bump 2018-05-06 00:43:05 +09:00
7872b3ec55 Add a new invoice event: expiredPaidPartial and fix some corner case for tolerance 2018-05-06 00:40:44 +09:00
27a0aebd12 Merge pull request #155 from Kukks/feature/order-tolerance
Payment Tolerance
2018-05-06 00:06:39 +09:00
366490516e Can filter with "exceptionstatus:", show the exception status on invoice list page 2018-05-05 23:25:09 +09:00
9a92646d4d add test and refactor for PR 2018-05-05 16:07:22 +02:00
b002c49dac Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-05 16:04:59 +02:00
3f4ec9ba80 simplify currency parsing if _ is forgotten and there is 6 letters 2018-05-05 22:59:53 +09:00
0290a5eacd update clightning 2018-05-05 22:46:07 +09:00
744734a6a1 Returns fallback feerate for coins not supporting fee rate query in NBXplorer 2018-05-05 22:19:36 +09:00
29f662f87c bump NBXplorer 2018-05-05 22:05:22 +09:00
af21f9f10c Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-05 08:49:16 +02:00
efdc99b9d1 Do not spam the logs about failed mail 2018-05-05 01:42:42 +09:00
4458e63c1a Break default DOGE rules in two, add some documentation about inverses 2018-05-05 01:34:08 +09:00
3225745115 bump 2018-05-05 01:01:39 +09:00
a325592106 Can match exact reverse 2018-05-05 01:00:19 +09:00
01069ed583 Remove unnecessary branching 2018-05-04 17:50:05 +02:00
0fc770bbb1 extract logic of accounting to accounting and remove bitpay breaking changes 2018-05-04 17:47:33 +02:00
dfb79ef96e Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-04 17:46:39 +02:00
4ebffc8d43 fix BIP70 bug 2018-05-05 00:44:02 +09:00
c2dad08fef Can solve inverse of currency pair 2018-05-05 00:40:54 +09:00
c3d73236e0 start work on payment tolerance feature 2018-05-04 16:15:34 +02:00
8a4da361fd Fix bug about invoice URL 2018-05-04 22:05:40 +09:00
57effe318b Fix missing URL for invoice 2018-05-04 21:41:50 +09:00
9325441693 fix typo 2018-05-04 16:09:43 +09:00
180341576b bump 2018-05-04 15:55:09 +09:00
e2533a93e3 Fix set email screen 2018-05-04 15:54:12 +09:00
14360bde78 Use rate directly from some exchanges, fix bug in ServerSettings 2018-05-04 15:36:10 +09:00
d793265bed Merge pull request #154 from rockstardev/master
Addressing several fixes that were assigned to me
2018-05-04 12:37:02 +09:00
0a449e1e8e Allowing custom HtmlTitle
Fix #96
2018-05-03 22:35:06 -05:00
74ccc34c9c Small enhancement on Rates page 2018-05-04 11:58:21 +09:00
674cd1486d Showing btcPaid once invoice is paid
Fix #144
2018-05-03 16:38:40 -05:00
ce12e87b70 Restoring QR Code for 2Fact authentication, fix #147 2018-05-03 16:13:50 -05:00
8f1324fdf3 Can clear email settings Fix #150 2018-05-04 02:16:12 +09:00
3ab69046b0 Add overpaid column Fix #149 2018-05-04 02:01:43 +09:00
6dc4bfaefe Make rate calculation scriptable 2018-05-04 01:46:52 +09:00
f460837f96 Make sure RateRules do not remove comments 2018-05-03 04:33:21 +09:00
34d0d3e011 make sure we can calculate the rate of default currencies 2018-05-03 03:40:10 +09:00
e57a488371 Refactor the RateProvider 2018-05-03 03:32:42 +09:00
43be1e191f Create the RateRules class for parsing rate calculation rules 2018-05-02 18:37:53 +09:00
eb975bf8fc Isolate Bitpay's code outside of middleware inside BitpayClaimsFilter 2018-04-30 22:28:00 +09:00
21bbf49640 Rewrite authorization enforcement and simplify the code 2018-04-30 22:00:43 +09:00
9339c7dff2 Make sure btcpay does not wait all the invoces to be cleaned to start 2018-04-30 15:39:47 +09:00
af0eb831a2 Remove useless code and rename file 2018-04-30 02:37:32 +09:00
1fc9a1a54b Move to a Claim based security 2018-04-30 02:33:42 +09:00
3954ce2137 fix (again) the broken hr.js 2018-04-30 01:32:15 +09:00
271de362cb fix broken checkout 2018-04-30 00:29:34 +09:00
d41474ebc8 Bump 2018-04-29 20:52:51 +09:00
5b0b3e30f4 Small rewrite 2018-04-29 20:50:54 +09:00
48a95457b6 fix boolean 2018-04-29 20:49:38 +09:00
7c0b26174f Fix theme manager incorrectly applying default theme if rootPath is specified 2018-04-29 20:48:17 +09:00
f0145142a4 Make sure that we don't authenticate call with bitpay auth methods on non bitpay calls 2018-04-29 20:32:43 +09:00
2848caff2e Support Legacy API Key authentication to Bitpay Invoice API 2018-04-29 18:28:04 +09:00
9e05ad787f Merge pull request #146 from 2pac1/master
Croatian translation
2018-04-29 12:00:48 +09:00
de39fa0aea Update hr.js 2018-04-28 18:46:55 +02:00
94ff77f2b2 Update hr.js 2018-04-28 18:44:55 +02:00
bb7dc1ed4a Update hr.js 2018-04-28 18:34:47 +02:00
c5e833ee79 Update hr.js 2018-04-28 18:33:34 +02:00
4397591134 Create hr.js 2018-04-28 18:27:51 +02:00
986c7b94f4 Update Checkout.cshtml 2018-04-28 18:13:44 +02:00
a6ef7387cf Update LanguageService.cs 2018-04-28 18:12:11 +02:00
95bdeacd93 Order exchanges in the list 2018-04-28 10:58:14 +09:00
07c2f6b810 Remove TokenRepository dependency from InvoiceControllerAPI 2018-04-28 02:51:20 +09:00
8ff81f1648 Use claim based authentication 2018-04-28 02:09:24 +09:00
c3ee43c228 Add ExchangeSharp 2018-04-27 12:15:29 +09:00
d85da28ca7 Merge pull request #145 from lepipele/dev-asynctask
Refactoring async while loop functionality
2018-04-27 11:56:54 +09:00
042142396d Refactoring code to adhere to naming guidelines 2018-04-26 21:52:04 -05:00
fbc4ca89aa Enapsulating Token per code review discussions 2018-04-26 21:44:21 -05:00
2e5d29064b Removing CssThemeManager dependency on ServerController
Using newly created BaseAsyncService to listen for database changes of theme setting
2018-04-26 21:39:43 -05:00
ef0b8376d3 Abstracting hosted service that has listen loop tasks 2018-04-26 21:39:43 -05:00
1fa1b74261 Center the last row of the PoS screen 2018-04-27 00:00:32 +09:00
101 changed files with 3834 additions and 1283 deletions

View File

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

View File

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

View File

@ -0,0 +1,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);
////////
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
IRateProviderFactory _RateProviders;
BTCPayRateProviderFactory _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviders,
BTCPayRateProviderFactory rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_UserManager = userManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class APIKeyData
{
[MaxLength(50)]
public string Id
{
get; set;
}
[MaxLength(50)]
public string StoreId
{
get; set;
}
}
}

View File

@ -86,6 +86,11 @@ namespace BTCPayServer.Data
get; set;
}
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);

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,553 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180429083930_legacyapikey")]
partial class legacyapikey
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class legacyapikey : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiKeys",
columns: table => new
{
Id = table.Column<string>(maxLength: 50, nullable: false),
StoreId = table.Column<string>(maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ApiKeys_StoreId",
table: "ApiKeys",
column: "StoreId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKeys");
}
}
}

View File

@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class RatesViewModel
{
public class TestResultViewModel
{
public string CurrencyPair { get; set; }
public string Rule { get; set; }
public bool Error { get; set; }
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
{
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
PreferredExchange = chosen.Value;
}
public List<TestResultViewModel> TestRateRules { get; set; }
public SelectList Exchanges { get; set; }
public bool ShowScripting { get; set; }
[Display(Name = "Rate rules")]
[MaxLength(2000)]
public string Script { get; set; }
public string DefaultScript { get; set; }
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by ...")]
[Range(0.01, 10.0)]
public double RateMultiplier
{
get;
set;
}
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
public string PreferredExchange { get; set; }
public string RateSource
{
get
{
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
}
}
}

View File

@ -1,5 +1,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;
}
}
}

View File

@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels
get;
set;
}
[Display(Name = "API Key")]
public string ApiKey { get; set; }
public string EncodedApiKey { get; set; }
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{
public class CurrencyPair
{
static readonly BTCPayNetworkProvider _NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
public CurrencyPair(string left, string right)
{
if (right == null)
throw new ArgumentNullException(nameof(right));
if (left == null)
throw new ArgumentNullException(nameof(left));
Right = right.ToUpperInvariant();
Left = left.ToUpperInvariant();
}
public string Left { get; private set; }
public string Right { get; private set; }
public static CurrencyPair Parse(string str)
{
if (!TryParse(str, out var result))
throw new FormatException("Invalid currency pair");
return result;
}
public static bool TryParse(string str, out CurrencyPair value)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
value = null;
str = str.Trim();
if (str.Length > 12)
return false;
var splitted = str.Split(new[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length == 2)
{
value = new CurrencyPair(splitted[0], splitted[1]);
return true;
}
else if (splitted.Length == 1)
{
var currencyPair = splitted[0];
if (currencyPair.Length < 6 || currencyPair.Length > 10)
return false;
if (currencyPair.Length == 6)
{
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
return true;
}
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = currencyPair.Substring(0, i);
var network = _NetworkProvider.GetNetwork(potentialCryptoName);
if (network != null)
{
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));
return true;
}
}
}
return false;
}
public override bool Equals(object obj)
{
CurrencyPair item = obj as CurrencyPair;
if (item == null)
return false;
return ToString().Equals(item.ToString(), StringComparison.OrdinalIgnoreCase);
}
public static bool operator ==(CurrencyPair a, CurrencyPair b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToString() == b.ToString();
}
public static bool operator !=(CurrencyPair a, CurrencyPair b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode(StringComparison.OrdinalIgnoreCase);
}
public override string ToString()
{
return $"{Left}_{Right}";
}
public CurrencyPair Inverse()
{
return new CurrencyPair(Right, Left);
}
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{
public class ExchangeRates : IEnumerable<ExchangeRate>
{
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
public ExchangeRates()
{
}
public ExchangeRates(IEnumerable<ExchangeRate> rates)
{
foreach (var rate in rates)
{
Add(rate);
}
}
List<ExchangeRate> _Rates = new List<ExchangeRate>();
public MultiValueDictionary<string, ExchangeRate> ByExchange
{
get;
private set;
} = new MultiValueDictionary<string, ExchangeRate>();
public void Add(ExchangeRate rate)
{
// 1 DOGE is always 1 DOGE
if (rate.CurrencyPair.Left == rate.CurrencyPair.Right)
return;
var key = $"({rate.Exchange}) {rate.CurrencyPair}";
if (_AllRates.TryAdd(key, rate))
{
_Rates.Add(rate);
ByExchange.Add(rate.Exchange, rate);
}
else
{
if (rate.Value.HasValue)
{
_AllRates[key].Value = rate.Value;
}
}
}
public IEnumerator<ExchangeRate> GetEnumerator()
{
return _Rates.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
{
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
rate.Value = value;
}
}
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
{
if (currencyPair.Left == currencyPair.Right)
return 1.0m;
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
return rate.Value;
}
return null;
}
}
public class ExchangeRate
{
public string Exchange { get; set; }
public CurrencyPair CurrencyPair { get; set; }
public decimal? Value { get; set; }
public override string ToString()
{
if (Value == null)
return $"{Exchange}({CurrencyPair})";
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
}
}
}

View File

@ -0,0 +1,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;
}
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Security
{
public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
public BTCPayClaimsFilter(
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
}
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
{
options.Filters.Add(typeof(BTCPayClaimsFilter));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (!context.HttpContext.GetIsBitpayAPI())
{
var identity = ((ClaimsIdentity)principal.Identity);
if (principal.IsInRole(Roles.ServerAdmin))
{
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
}
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
{
var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (claim != null)
{
var store = await _StoreRepository.FindStore((string)storeId, claim.Value);
if (store == null)
context.Result = new ChallengeResult(Policies.CookieAuthentication);
else
{
context.HttpContext.SetStoreData(store);
if (store != null)
{
identity.AddClaims(store.GetClaims());
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,196 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBitpayClient.Extensions;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Http.Internal;
namespace BTCPayServer.Security
{
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayClaimsFilter(
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
_TokenRepository = tokenRepository;
}
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
{
options.Filters.Add(typeof(BitpayClaimsFilter));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (context.HttpContext.GetIsBitpayAPI())
{
var bitpayAuth = context.HttpContext.GetBitpayAuth();
string storeId = null;
var failedAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
{
Logs.PayServer.LogDebug("BitId signature check failed");
failedAuth = true;
}
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
}
if (storeId != null)
{
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
identity.AddClaim(new Claim(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;
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Security
{
public static class Policies
{
public const string CookieAuthentication = "Identity.Application";
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{
AddClaim(options, CanUseStore.Key);
AddClaim(options, CanModifyStoreSettings.Key);
AddClaim(options, CanModifyServerSettings.Key);
return options;
}
private static void AddClaim(AuthorizationOptions options, string key)
{
options.AddPolicy(key, o => o.RequireClaim(key));
}
public class CanModifyServerSettings
{
public const string Key = "btcpay.store.canmodifyserversettings";
}
public class CanUseStore
{
public const string Key = "btcpay.store.canusestore";
}
public class CanModifyStoreSettings
{
public const string Key = "btcpay.store.canmodifystoresettings";
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
@ -21,29 +22,6 @@ namespace BTCPayServer.Services.Rates
}
}
public class CoinAverageRateProviderDescription : RateProviderDescription
{
public CoinAverageRateProviderDescription(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider)
{
return new CoinAverageRateProvider(CryptoCode)
{
Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>()
};
}
IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider)
{
return CreateRateProvider(serviceProvider);
}
}
public class GetExchangeTickersResponse
{
public class Exchange
@ -69,18 +47,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;

View File

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

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
namespace BTCPayServer.Services.Rates
{
public class ExchangeSharpRateProvider : IRateProvider
{
readonly ExchangeAPI _ExchangeAPI;
readonly string _ExchangeName;
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
{
if (exchangeAPI == null)
throw new ArgumentNullException(nameof(exchangeAPI));
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
_ExchangeAPI = exchangeAPI;
_ExchangeName = exchangeName;
ReverseCurrencyPair = reverseCurrencyPair;
}
public bool ReverseCurrencyPair
{
get; set;
}
public async Task<ExchangeRates> GetRatesAsync()
{
await new SynchronizationContextRemover();
var rates = await _ExchangeAPI.GetTickersAsync();
lock (notFoundSymbols)
{
var exchangeRates =
rates.Select(t => CreateExchangeRate(t))
.Where(t => t != null)
.ToArray();
return new ExchangeRates(exchangeRates);
}
}
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
HashSet<string> notFoundSymbols = new HashSet<string>();
private ExchangeRate CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
{
if (notFoundSymbols.Contains(ticker.Key))
return null;
try
{
var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key);
if (!CurrencyPair.TryParse(tickerName, out var pair))
{
notFoundSymbols.Add(ticker.Key);
return null;
}
if(ReverseCurrencyPair)
pair = new CurrencyPair(pair.Right, pair.Left);
var rate = new ExchangeRate();
rate.CurrencyPair = pair;
rate.Exchange = _ExchangeName;
rate.Value = ticker.Value.Bid;
return rate;
}
catch (ArgumentException)
{
notFoundSymbols.Add(ticker.Key);
return null;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@model RatesViewModel
@model BTCPayServer.Models.ServerViewModels.RatesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Rates);
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View 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