Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
8a4da361fd | |||
57effe318b | |||
9325441693 | |||
180341576b | |||
e2533a93e3 | |||
14360bde78 | |||
d793265bed | |||
0a449e1e8e | |||
74ccc34c9c | |||
674cd1486d | |||
ce12e87b70 | |||
8f1324fdf3 | |||
3ab69046b0 | |||
6dc4bfaefe | |||
f460837f96 | |||
34d0d3e011 | |||
e57a488371 | |||
43be1e191f | |||
eb975bf8fc | |||
21bbf49640 | |||
9339c7dff2 | |||
af0eb831a2 | |||
1fc9a1a54b | |||
3954ce2137 |
@ -2,8 +2,11 @@
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -104,15 +107,6 @@ namespace BTCPayServer.Tests
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
if (MockRates)
|
||||
{
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
|
||||
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
|
||||
mockRates.AddMock(btc);
|
||||
mockRates.AddMock(ltc);
|
||||
s.AddSingleton<IRateProviderFactory>(mockRates);
|
||||
}
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
@ -126,6 +120,30 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
|
||||
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
|
||||
rateProvider.DirectProviders.Clear();
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
Value = 5000m
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
Value = 4500m
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
Value = 500m
|
||||
});
|
||||
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
|
||||
}
|
||||
|
||||
public string HostName
|
||||
@ -142,7 +160,7 @@ namespace BTCPayServer.Tests
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null) where T : Controller
|
||||
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
@ -150,7 +168,11 @@ namespace BTCPayServer.Tests
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||
}
|
||||
if(storeId != null)
|
||||
{
|
||||
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||
}
|
||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||
var provider = scope.CreateScope().ServiceProvider;
|
||||
|
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
}
|
||||
}
|
125
BTCPayServer.Tests/RateRulesTest.cs
Normal file
125
BTCPayServer.Tests/RateRulesTest.cs
Normal file
@ -0,0 +1,125 @@
|
||||
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);
|
||||
////////
|
||||
}
|
||||
}
|
||||
}
|
@ -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 :
|
||||
|
@ -35,6 +35,8 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -108,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);
|
||||
@ -306,9 +297,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()
|
||||
{
|
||||
@ -322,7 +313,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)));
|
||||
}
|
||||
}
|
||||
@ -468,8 +459,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",
|
||||
@ -527,13 +518,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);
|
||||
}
|
||||
}
|
||||
@ -630,13 +623,13 @@ namespace BTCPayServer.Tests
|
||||
// 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(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(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
|
||||
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.NotEqual(apiKey, apiKey2);
|
||||
////////
|
||||
@ -685,10 +678,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,
|
||||
@ -724,11 +717,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()
|
||||
@ -804,6 +797,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()
|
||||
{
|
||||
@ -963,10 +1016,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()
|
||||
{
|
||||
@ -1126,8 +1179,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);
|
||||
@ -1231,40 +1282,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)
|
||||
|
@ -44,7 +44,6 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public RateProviderDescription DefaultRateProvider { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
@ -62,6 +61,7 @@ namespace BTCPayServer
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -14,9 +14,6 @@ namespace BTCPayServer
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProviderDescription("BTC");
|
||||
var bitpay = new BitpayRateProviderDescription();
|
||||
var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
@ -24,7 +21,6 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -20,7 +20,7 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
||||
DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" },
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
|
@ -20,7 +20,6 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -86,7 +86,11 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
||||
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
|
||||
{
|
||||
if (cryptoCode == "XBT")
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
return network;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.95</Version>
|
||||
<Version>1.0.2.2</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.0" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
@ -40,7 +40,7 @@
|
||||
<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" />
|
||||
|
@ -16,10 +16,11 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
|
@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type.Value.ToString() != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
|
@ -22,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)
|
||||
{
|
||||
@ -390,11 +393,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 +408,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 +462,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 +484,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
|
@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
IRateProviderFactory _RateProviders;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviders,
|
||||
BTCPayRateProviderFactory rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_UserManager = userManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
@ -111,6 +112,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 +137,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 +202,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 +210,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 +246,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 +270,6 @@ namespace BTCPayServer.Controllers
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if (transactionSpeed == null)
|
||||
|
@ -21,10 +21,11 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
{
|
||||
|
@ -8,17 +8,19 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
@ -32,45 +34,90 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
RateRules rules = null;
|
||||
if (storeId != null)
|
||||
if(storeId == null || currencyPairs == null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rules = store.GetStoreBlob().GetRateRules();
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
result.StatusCode = 404;
|
||||
return result;
|
||||
}
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
|
||||
foreach(var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if(!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
CryptoCode = r.Pair.Left,
|
||||
Code = r.Pair.Right,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
|
||||
Value = r.Value.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
|
||||
public class Rate
|
||||
{
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "cryptoCode")]
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "rate")]
|
||||
public decimal Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,15 +19,15 @@ 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 BTCPayRateProviderFactory _RateProviderFactory;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
@ -99,7 +99,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider("BTC")
|
||||
return new CoinAverageRateProvider()
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
@ -241,10 +241,16 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (command == "Test")
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
try
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(model.Settings.From)
|
||||
|| string.IsNullOrWhiteSpace(model.TestEmail)
|
||||
|| string.IsNullOrWhiteSpace(model.Settings.Login)
|
||||
|| string.IsNullOrWhiteSpace(model.Settings.Server))
|
||||
{
|
||||
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";
|
||||
@ -255,11 +261,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
else // if(command == "Save")
|
||||
{
|
||||
ModelState.Remove(nameof(model.TestEmail));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
model.StatusMessage = "Email settings saved";
|
||||
return View(model);
|
||||
|
@ -21,9 +21,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
|
||||
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
@ -60,7 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
@ -188,7 +188,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
|
@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
||||
public IActionResult AddLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
@ -4,6 +4,8 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -19,6 +21,7 @@ using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -27,11 +30,12 @@ using System.Threading.Tasks;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = StorePolicies.OwnStore)]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateFactory;
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
@ -45,12 +49,14 @@ namespace BTCPayServer.Controllers
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayRateProviderFactory rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
@ -93,13 +99,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
||||
public IActionResult Wallet(string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.ServerUrl = GetStoreUrl(StoreData.Id);
|
||||
model.CryptoCurrency = cryptoCode;
|
||||
return View(model);
|
||||
}
|
||||
@ -111,17 +114,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId)
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
await FillUsers(storeId, vm);
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(storeId);
|
||||
vm.StoreId = storeId;
|
||||
var users = await _Repo.GetStoreUsers(StoreData.Id);
|
||||
vm.StoreId = StoreData.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
@ -130,11 +133,20 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public StoreData StoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(storeId, vm);
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
@ -150,7 +162,7 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
||||
if (!await _Repo.AddStoreUser(StoreData.Id, user.Id, vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||
return View(vm);
|
||||
@ -161,19 +173,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
public async Task<IActionResult> DeleteStoreUser(string userId)
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove store user",
|
||||
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
|
||||
Description = $"Are you sure to remove access to remove access to {user.Email}?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
@ -188,15 +197,151 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId)
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
model.AvailableExchanges = GetSupportedExchanges();
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
RateRules rules = null;
|
||||
if (model.ShowScripting)
|
||||
{
|
||||
if (!RateRules.TryParse(model.Script, out rules, out var errors))
|
||||
{
|
||||
errors = errors ?? new List<RateRulesErrors>();
|
||||
var errorString = String.Join(", ", errors.ToArray());
|
||||
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
{
|
||||
blob.RateScript = rules.ToString();
|
||||
ModelState.Remove(nameof(model.Script));
|
||||
model.Script = blob.RateScript;
|
||||
}
|
||||
}
|
||||
rules = blob.GetRateRules(_NetworkProvider);
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.ScriptTest))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
|
||||
return View(model);
|
||||
}
|
||||
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var pairs = new List<CurrencyPair>();
|
||||
foreach (var pair in splitted)
|
||||
{
|
||||
if (!CurrencyPair.TryParse(pair, out var currencyPair))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
|
||||
return View(model);
|
||||
}
|
||||
pairs.Add(currencyPair);
|
||||
}
|
||||
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||
foreach (var fetch in fetchs)
|
||||
{
|
||||
var testResult = await (fetch.Value);
|
||||
testResults.Add(new RatesViewModel.TestResultViewModel()
|
||||
{
|
||||
CurrencyPair = fetch.Key.ToString(),
|
||||
Error = testResult.Errors.Count != 0,
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
model.TestRateRules = testResults;
|
||||
return View(model);
|
||||
}
|
||||
else // command == Save
|
||||
{
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Rate settings updated";
|
||||
}
|
||||
return RedirectToAction(nameof(Rates), new
|
||||
{
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates/confirm")]
|
||||
public IActionResult ShowRateRules(bool scripting)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Continue",
|
||||
Title = "Rate rule scripting",
|
||||
Description = scripting ?
|
||||
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates/confirm")]
|
||||
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||
{
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.RateScripting = scripting;
|
||||
blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
StoreData.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Rate rules scripting activated";
|
||||
return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public IActionResult CheckoutExperience()
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
@ -204,12 +349,13 @@ namespace BTCPayServer.Controllers
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
|
||||
public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
|
||||
{
|
||||
CurrencyValue lightningMaxValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||
@ -228,16 +374,12 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
|
||||
}
|
||||
}
|
||||
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
bool needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
@ -253,33 +395,33 @@ namespace BTCPayServer.Controllers
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||
if (store.SetStoreBlob(blob))
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(CheckoutExperience), new
|
||||
{
|
||||
storeId = storeId
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId)
|
||||
public IActionResult UpdateStore()
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
@ -288,7 +430,6 @@ namespace BTCPayServer.Controllers
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
return View(vm);
|
||||
}
|
||||
@ -329,81 +470,56 @@ 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;
|
||||
|
||||
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)
|
||||
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
return _CoinAverage.AvailableExchanges
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
@ -415,10 +531,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()
|
||||
{
|
||||
@ -428,7 +544,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = t.Value
|
||||
}).ToArray();
|
||||
|
||||
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(storeId)).FirstOrDefault();
|
||||
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(StoreData.Id)).FirstOrDefault();
|
||||
if (model.ApiKey == null)
|
||||
model.EncodedApiKey = "*API Key*";
|
||||
else
|
||||
@ -439,24 +555,31 @@ namespace BTCPayServer.Controllers
|
||||
[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()
|
||||
@ -497,11 +620,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;
|
||||
@ -510,20 +642,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
|
||||
@ -533,20 +670,24 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId)
|
||||
public async Task<IActionResult> GenerateAPIKey()
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _TokenRepository.GenerateLegacyAPIKey(storeId);
|
||||
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);
|
||||
@ -557,7 +698,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
@ -565,7 +706,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
|
||||
@ -576,19 +717,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);
|
||||
@ -614,6 +758,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
if (User.Identity.AuthenticationType != Policies.CookieAuthentication)
|
||||
return null;
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -15,7 +16,7 @@ using NBXplorer.DerivationStrategy;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UserStoresController : Controller
|
||||
{
|
||||
@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.Role == StoreRoles.Owner,
|
||||
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
|
||||
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
|
@ -15,6 +15,10 @@ 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
|
||||
{
|
||||
@ -152,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;
|
||||
@ -179,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)
|
||||
@ -193,9 +225,9 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
public class RateRule_Obsolete
|
||||
{
|
||||
public RateRule()
|
||||
public RateRule_Obsolete()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
@ -247,8 +279,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()
|
||||
{
|
||||
@ -262,7 +294,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))]
|
||||
@ -274,6 +306,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;
|
||||
@ -289,12 +326,44 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public RateRules GetRateRules()
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
return new RateRules(RateRules)
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
|
||||
{
|
||||
PreferredExchange = PreferredExchange
|
||||
};
|
||||
return GetDefaultRateRules(networkProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
rules.GlobalMultiplier = GetRateMultiplier();
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
|
||||
public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (var network in networkProvider.GetAll())
|
||||
{
|
||||
if (network.DefaultRateRules.Length != 0)
|
||||
{
|
||||
builder.AppendLine($"// Default rate rules for {network.CryptoCode}");
|
||||
foreach (var line in network.DefaultRateRules)
|
||||
{
|
||||
builder.AppendLine(line);
|
||||
}
|
||||
builder.AppendLine($"////////");
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange;
|
||||
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
|
||||
|
||||
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
|
||||
rules.GlobalMultiplier = GetRateMultiplier();
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,12 +104,6 @@ namespace BTCPayServer
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
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();
|
||||
@ -167,12 +161,29 @@ namespace BTCPayServer
|
||||
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)
|
||||
{
|
||||
@ -189,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 + "');");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -17,15 +17,15 @@ namespace BTCPayServer.HostedServices
|
||||
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;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
@ -40,11 +40,15 @@ namespace BTCPayServer.HostedServices
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
_coinAverageSettings.AvailableExchanges = tickers
|
||||
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach(var item in tickers
|
||||
.Exchanges
|
||||
.Select(c => (c.DisplayName, c.Name))
|
||||
.ToArray();
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
{
|
||||
exchanges.Add(item);
|
||||
}
|
||||
_coinAverageSettings.AvailableExchanges = exchanges;
|
||||
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
||||
}
|
||||
|
||||
|
@ -38,55 +38,13 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public static class BTCPayServerServices
|
||||
{
|
||||
public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public OwnStoreAuthorizationRequirement()
|
||||
{
|
||||
}
|
||||
|
||||
public OwnStoreAuthorizationRequirement(string role)
|
||||
{
|
||||
Role = role;
|
||||
}
|
||||
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class OwnStoreHandler : AuthorizationHandler<OwnStoreAuthorizationRequirement>
|
||||
{
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_StoreRepository = storeRepository;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement)
|
||||
{
|
||||
object storeId = null;
|
||||
if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
|
||||
context.Succeed(requirement);
|
||||
else if (storeId != null)
|
||||
{
|
||||
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
|
||||
if (user != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, user);
|
||||
if (store != null)
|
||||
if (requirement.Role == null || requirement.Role == store.Role)
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||
@ -110,7 +68,6 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -160,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 =>
|
||||
@ -169,30 +128,17 @@ namespace BTCPayServer.Hosting
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
|
||||
services.TryAddSingleton<BTCPayRateProviderFactory>();
|
||||
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
services.AddAuthorization(o =>
|
||||
{
|
||||
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
||||
});
|
||||
|
||||
o.AddPolicy(StorePolicies.OwnStore, builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
|
||||
});
|
||||
});
|
||||
|
||||
// bundling
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
|
||||
services.AddBundles();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
|
@ -6,45 +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 System.Security.Claims;
|
||||
using BTCPayServer.Services;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class BTCPayMiddleware
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
StoreRepository _StoreRepository;
|
||||
RequestDelegate _Next;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
TokenRepository tokenRepo,
|
||||
StoreRepository storeRepo,
|
||||
BTCPayServerOptions options)
|
||||
{
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_StoreRepository = storeRepo;
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
@ -61,39 +41,7 @@ namespace BTCPayServer.Hosting
|
||||
httpContext.SetIsBitpayAPI(isBitpayAPI);
|
||||
if (isBitpayAPI)
|
||||
{
|
||||
|
||||
string storeId = null;
|
||||
var failedAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
storeId = await CheckBitId(httpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||
if (!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(httpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
httpContext.SetStoreData(store);
|
||||
}
|
||||
else if (failedAuth)
|
||||
{
|
||||
throw new BitpayHttpException(401, "Can't access to store");
|
||||
}
|
||||
httpContext.SetBitpayAuth(bitpayAuth);
|
||||
}
|
||||
await _Next(httpContext);
|
||||
}
|
||||
@ -255,109 +203,5 @@ namespace BTCPayServer.Hosting
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,5 +19,6 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ButtonClass { get; set; } = "btn-danger";
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string Address { get; internal set; }
|
||||
public string Rate { get; internal set; }
|
||||
public string PaymentUrl { get; internal set; }
|
||||
public string Overpaid { get; set; }
|
||||
}
|
||||
public class AddressModel
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
public string HtmlTitle { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string DefaultLang { get; set; }
|
||||
|
@ -18,8 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
|
||||
[EmailAddress]
|
||||
public string TestEmail
|
||||
{
|
||||
|
@ -48,6 +48,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Url]
|
||||
public string CustomLogo { get; set; }
|
||||
|
||||
[Display(Name = "Custom HTML title to display on Checkout page")]
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
|
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
66
BTCPayServer/Models/StoreViewModels/RatesViewModel.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class RatesViewModel
|
||||
{
|
||||
public class TestResultViewModel
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
public string Rule { get; set; }
|
||||
public bool Error { get; set; }
|
||||
}
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
}
|
||||
|
||||
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
|
||||
public bool ShowScripting { get; set; }
|
||||
|
||||
[Display(Name = "Rate rules")]
|
||||
[MaxLength(2000)]
|
||||
public string Script { get; set; }
|
||||
public string DefaultScript { get; set; }
|
||||
public string ScriptTest { get; set; }
|
||||
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Multiply the rate by ...")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System;
|
||||
@ -12,11 +13,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public class DerivationScheme
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
@ -49,36 +45,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? "coinaverage";
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
}
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
|
||||
[Display(Name = "Multiply the original rate by ...")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||
[Range(1, 60 * 24 * 24)]
|
||||
public int InvoiceExpiration
|
||||
|
94
BTCPayServer/Rating/CurrencyPair.cs
Normal file
94
BTCPayServer/Rating/CurrencyPair.cs
Normal file
@ -0,0 +1,94 @@
|
||||
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;
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
96
BTCPayServer/Rating/ExchangeRates.cs
Normal file
96
BTCPayServer/Rating/ExchangeRates.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public class ExchangeRates : IEnumerable<ExchangeRate>
|
||||
{
|
||||
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
|
||||
public ExchangeRates()
|
||||
{
|
||||
|
||||
}
|
||||
public ExchangeRates(IEnumerable<ExchangeRate> rates)
|
||||
{
|
||||
foreach (var rate in rates)
|
||||
{
|
||||
Add(rate);
|
||||
}
|
||||
}
|
||||
List<ExchangeRate> _Rates = new List<ExchangeRate>();
|
||||
public MultiValueDictionary<string, ExchangeRate> ByExchange
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new MultiValueDictionary<string, ExchangeRate>();
|
||||
|
||||
public void Add(ExchangeRate rate)
|
||||
{
|
||||
// 1 DOGE is always 1 DOGE
|
||||
if (rate.CurrencyPair.Left == rate.CurrencyPair.Right)
|
||||
return;
|
||||
var key = $"({rate.Exchange}) {rate.CurrencyPair}";
|
||||
if (_AllRates.TryAdd(key, rate))
|
||||
{
|
||||
_Rates.Add(rate);
|
||||
ByExchange.Add(rate.Exchange, rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rate.Value.HasValue)
|
||||
{
|
||||
_AllRates[key].Value = rate.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<ExchangeRate> GetEnumerator()
|
||||
{
|
||||
return _Rates.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
|
||||
{
|
||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||
{
|
||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||
if (rate != null)
|
||||
rate.Value = value;
|
||||
}
|
||||
}
|
||||
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
|
||||
{
|
||||
if (currencyPair.Left == currencyPair.Right)
|
||||
return 1.0m;
|
||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||
{
|
||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||
if (rate != null)
|
||||
return rate.Value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public class ExchangeRate
|
||||
{
|
||||
public string Exchange { get; set; }
|
||||
public CurrencyPair CurrencyPair { get; set; }
|
||||
public decimal? Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Value == null)
|
||||
return $"{Exchange}({CurrencyPair})";
|
||||
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
}
|
499
BTCPayServer/Rating/RateRules.cs
Normal file
499
BTCPayServer/Rating/RateRules.cs
Normal file
@ -0,0 +1,499 @@
|
||||
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 candidates = new List<(CurrencyPair Pair, int Prioriy, ExpressionSyntax Expression)>();
|
||||
foreach (var pair in new[]
|
||||
{
|
||||
(Pair: p, Priority: 0),
|
||||
(Pair: new CurrencyPair(p.Left, "X"), Priority: 1),
|
||||
(Pair: new CurrencyPair("X", p.Right), Priority: 1),
|
||||
(Pair: new CurrencyPair("X", "X"), Priority: 2)
|
||||
})
|
||||
{
|
||||
if (ruleList.ExpressionsByPair.TryGetValue(pair.Pair, out var expression))
|
||||
{
|
||||
candidates.Add((pair.Pair, pair.Priority, expression.Expression));
|
||||
}
|
||||
}
|
||||
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.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 = 6;
|
||||
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
|
||||
{
|
||||
var ctx = Context.Peek();
|
||||
|
||||
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? ctx.Left : currentPair.Left,
|
||||
right: currentPair.Right == "X" ? ctx.Right : currentPair.Right);
|
||||
if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD)
|
||||
{
|
||||
ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName });
|
||||
return SyntaxFactory.IdentifierName(replacedPair.ToString());
|
||||
}
|
||||
else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD
|
||||
{
|
||||
var bestCandidate = parent.FindBestCandidate(replacedPair);
|
||||
if (Context.Count > MaxNestedCount)
|
||||
{
|
||||
Errors.Add(RateRulesErrors.TooMuchNestedCalls);
|
||||
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||
}
|
||||
Context.Push(replacedPair);
|
||||
var replaced = Visit(bestCandidate);
|
||||
if (replaced is ExpressionSyntax expression)
|
||||
{
|
||||
var hasBinaryOps = new HasBinaryOperations();
|
||||
hasBinaryOps.Visit(expression);
|
||||
if (hasBinaryOps.Result)
|
||||
{
|
||||
replaced = SyntaxFactory.ParenthesizedExpression(expression);
|
||||
}
|
||||
}
|
||||
Context.Pop();
|
||||
if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls))
|
||||
{
|
||||
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||
}
|
||||
return replaced;
|
||||
}
|
||||
}
|
||||
return base.VisitIdentifierName(node);
|
||||
}
|
||||
}
|
||||
private SyntaxNode expression;
|
||||
FlattenExpressionRewriter flatten;
|
||||
|
||||
public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate)
|
||||
{
|
||||
flatten = new FlattenExpressionRewriter(parent, currencyPair);
|
||||
this.expression = flatten.Visit(candidate);
|
||||
}
|
||||
|
||||
public ExchangeRates ExchangeRates
|
||||
{
|
||||
get
|
||||
{
|
||||
return flatten.ExchangeRates;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool Reevaluate()
|
||||
{
|
||||
_Value = null;
|
||||
_EvaluatedNode = null;
|
||||
_Evaluated = null;
|
||||
Errors.Clear();
|
||||
|
||||
var rewriter = new ReplaceExchangeRateRewriter();
|
||||
rewriter.Rates = ExchangeRates;
|
||||
var result = rewriter.Visit(this.expression);
|
||||
Errors.AddRange(rewriter.Errors);
|
||||
_Evaluated = result.NormalizeWhitespace("", "\n").ToString();
|
||||
if (HasError)
|
||||
return false;
|
||||
|
||||
var calculate = new CalculateWalker();
|
||||
calculate.Visit(result);
|
||||
if (calculate.Values.Count != 1 || calculate.Errors.Count != 0)
|
||||
{
|
||||
Errors.AddRange(calculate.Errors);
|
||||
return false;
|
||||
}
|
||||
_Value = calculate.Values.Pop();
|
||||
_EvaluatedNode = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private readonly HashSet<RateRulesErrors> _Errors = new HashSet<RateRulesErrors>();
|
||||
public HashSet<RateRulesErrors> Errors
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Errors;
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxNode _EvaluatedNode;
|
||||
string _Evaluated;
|
||||
public bool HasError
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Errors.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string ToString(bool evaluated)
|
||||
{
|
||||
if (!evaluated)
|
||||
return ToString();
|
||||
if (_Evaluated == null)
|
||||
return "Call Evaluate() first";
|
||||
return _Evaluated;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return expression.NormalizeWhitespace("", "\n").ToString();
|
||||
}
|
||||
|
||||
decimal? _Value;
|
||||
public decimal? Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
BTCPayServer/Security/BTCPayClaimsFilter.cs
Normal file
66
BTCPayServer/Security/BTCPayClaimsFilter.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
|
||||
public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
StoreRepository _StoreRepository;
|
||||
public BTCPayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||
{
|
||||
options.Filters.Add(typeof(BTCPayClaimsFilter));
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (!context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var identity = ((ClaimsIdentity)principal.Identity);
|
||||
if (principal.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
|
||||
}
|
||||
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
|
||||
{
|
||||
var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
|
||||
if (claim != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, claim.Value);
|
||||
if (store == null)
|
||||
context.Result = new ChallengeResult(Policies.CookieAuthentication);
|
||||
else
|
||||
{
|
||||
context.HttpContext.SetStoreData(store);
|
||||
if (store != null)
|
||||
{
|
||||
identity.AddClaims(store.GetClaims());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
196
BTCPayServer/Security/BitpayClaimsFilter.cs
Normal file
196
BTCPayServer/Security/BitpayClaimsFilter.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBitpayClient.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
StoreRepository _StoreRepository;
|
||||
TokenRepository _TokenRepository;
|
||||
|
||||
public BitpayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_TokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||
{
|
||||
options.Filters.Add(typeof(BitpayClaimsFilter));
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var bitpayAuth = context.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
var failedAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
|
||||
{
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
context.HttpContext.SetStoreData(store);
|
||||
}
|
||||
else if (failedAuth)
|
||||
{
|
||||
throw new BitpayHttpException(401, "Can't access to store");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string storeId = null;
|
||||
string body = string.Empty;
|
||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = httpContext.Request.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var sin = key.GetBitIDSIN();
|
||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.SIN, sin));
|
||||
|
||||
string token = null;
|
||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||
{
|
||||
token = tokenValues[0];
|
||||
}
|
||||
|
||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||
{
|
||||
try
|
||||
{
|
||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
if (bitToken == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
storeId = bitToken.StoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
return storeId;
|
||||
}
|
||||
|
||||
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||
{
|
||||
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string apiKey = null;
|
||||
try
|
||||
{
|
||||
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/Security/Policies.cs
Normal file
38
BTCPayServer/Security/Policies.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public static class Policies
|
||||
{
|
||||
public const string CookieAuthentication = "Identity.Application";
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
AddClaim(options, CanUseStore.Key);
|
||||
AddClaim(options, CanModifyStoreSettings.Key);
|
||||
AddClaim(options, CanModifyServerSettings.Key);
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void AddClaim(AuthorizationOptions options, string key)
|
||||
{
|
||||
options.AddPolicy(key, o => o.RequireClaim(key));
|
||||
}
|
||||
|
||||
public class CanModifyServerSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifyserversettings";
|
||||
}
|
||||
public class CanUseStore
|
||||
{
|
||||
public const string Key = "btcpay.store.canusestore";
|
||||
}
|
||||
public class CanModifyStoreSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||
}
|
||||
}
|
||||
}
|
@ -337,15 +337,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,23 +360,22 @@ 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)
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
|
||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={cryptoInfo.Url}",
|
||||
BIP72b = $"{scheme}:?r={cryptoInfo.Url}",
|
||||
BIP73 = cryptoInfo.Url,
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
}
|
||||
var paymentId = info.GetId();
|
||||
|
||||
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
@ -386,7 +386,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 +395,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 +403,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 +429,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;
|
||||
@ -635,20 +621,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 +645,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 +662,15 @@ 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;
|
||||
return accounting;
|
||||
}
|
||||
|
||||
@ -762,7 +743,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);
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -3,13 +3,36 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BTCPayRateProviderFactory : IRateProviderFactory
|
||||
public class ExchangeException
|
||||
{
|
||||
public Exception Exception { get; set; }
|
||||
public string ExchangeName { get; set; }
|
||||
}
|
||||
public class RateResult
|
||||
{
|
||||
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
|
||||
public string Rule { get; set; }
|
||||
public string EvaluatedRule { get; set; }
|
||||
public HashSet<RateRulesErrors> Errors { get; set; }
|
||||
public decimal? Value { get; set; }
|
||||
public bool Cached { get; internal set; }
|
||||
}
|
||||
|
||||
public class BTCPayRateProviderFactory
|
||||
{
|
||||
class QueryRateResult
|
||||
{
|
||||
public bool CachedResult { get; set; }
|
||||
public List<ExchangeException> Exceptions { get; set; }
|
||||
public ExchangeRates ExchangeRates { get; set; }
|
||||
}
|
||||
IMemoryCache _Cache;
|
||||
private IOptions<MemoryCacheOptions> _CacheOptions;
|
||||
|
||||
@ -20,18 +43,57 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Cache;
|
||||
}
|
||||
}
|
||||
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, IServiceProvider serviceProvider)
|
||||
CoinAverageSettings _CoinAverageSettings;
|
||||
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
|
||||
BTCPayNetworkProvider btcpayNetworkProvider,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
if (cacheOptions == null)
|
||||
throw new ArgumentNullException(nameof(cacheOptions));
|
||||
_CoinAverageSettings = coinAverageSettings;
|
||||
_Cache = new MemoryCache(cacheOptions);
|
||||
_CacheOptions = cacheOptions;
|
||||
// We use 15 min because of limits with free version of bitcoinaverage
|
||||
CacheSpan = TimeSpan.FromMinutes(15.0);
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.btcpayNetworkProvider = btcpayNetworkProvider;
|
||||
InitExchanges();
|
||||
}
|
||||
|
||||
IServiceProvider serviceProvider;
|
||||
public bool UseCoinAverageAsFallback { get; set; } = true;
|
||||
|
||||
private void InitExchanges()
|
||||
{
|
||||
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
|
||||
DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
|
||||
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
|
||||
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
|
||||
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
|
||||
|
||||
// Handmade providers
|
||||
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
|
||||
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true));
|
||||
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
|
||||
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
|
||||
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
|
||||
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
|
||||
}
|
||||
|
||||
|
||||
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
|
||||
public Dictionary<string, IRateProvider> DirectProviders
|
||||
{
|
||||
get
|
||||
{
|
||||
return _DirectProviders;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BTCPayNetworkProvider btcpayNetworkProvider;
|
||||
TimeSpan _CacheSpan;
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
@ -51,45 +113,87 @@ namespace BTCPayServer.Services.Rates
|
||||
_Cache = new MemoryCache(_CacheOptions);
|
||||
}
|
||||
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
|
||||
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
|
||||
{
|
||||
rules = rules ?? new RateRules();
|
||||
var rateProvider = GetDefaultRateProvider(network);
|
||||
if (!rules.PreferredExchange.IsCoinAverage())
|
||||
{
|
||||
rateProvider = CreateExchangeRateProvider(network, rules.PreferredExchange);
|
||||
}
|
||||
rateProvider = CreateCachedRateProvider(network, rateProvider, rules.PreferredExchange);
|
||||
return new TweakRateProvider(network, rateProvider, rules);
|
||||
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
|
||||
}
|
||||
|
||||
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
|
||||
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
|
||||
{
|
||||
if (rules == null)
|
||||
throw new ArgumentNullException(nameof(rules));
|
||||
|
||||
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
|
||||
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
|
||||
var consolidatedRates = new ExchangeRates();
|
||||
|
||||
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
|
||||
{
|
||||
var dependentQueries = new List<Task<QueryRateResult>>();
|
||||
foreach (var requiredExchange in i.RateRule.ExchangeRates)
|
||||
{
|
||||
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
|
||||
{
|
||||
fetching = QueryRates(requiredExchange.Exchange);
|
||||
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
|
||||
}
|
||||
dependentQueries.Add(fetching);
|
||||
}
|
||||
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
|
||||
}
|
||||
return fetchingRates;
|
||||
}
|
||||
|
||||
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
|
||||
{
|
||||
var result = new RateResult();
|
||||
result.Cached = true;
|
||||
foreach (var queryAsync in dependentQueries)
|
||||
{
|
||||
var query = await queryAsync;
|
||||
if (!query.CachedResult)
|
||||
result.Cached = false;
|
||||
result.ExchangeExceptions.AddRange(query.Exceptions);
|
||||
foreach (var rule in query.ExchangeRates)
|
||||
{
|
||||
rateRule.ExchangeRates.Add(rule);
|
||||
}
|
||||
}
|
||||
rateRule.Reevaluate();
|
||||
result.Value = rateRule.Value;
|
||||
result.Errors = rateRule.Errors;
|
||||
result.EvaluatedRule = rateRule.ToString(true);
|
||||
result.Rule = rateRule.ToString(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private async Task<QueryRateResult> QueryRates(string exchangeName)
|
||||
{
|
||||
List<IRateProvider> providers = new List<IRateProvider>();
|
||||
|
||||
if(exchange == "quadrigacx")
|
||||
if (DirectProviders.TryGetValue(exchangeName, out var directProvider))
|
||||
providers.Add(directProvider);
|
||||
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
|
||||
{
|
||||
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
|
||||
providers.Add(new CoinAverageRateProvider()
|
||||
{
|
||||
Exchange = exchangeName,
|
||||
Authenticator = _CoinAverageSettings
|
||||
});
|
||||
}
|
||||
|
||||
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
|
||||
coinAverage.Exchange = exchange;
|
||||
providers.Add(coinAverage);
|
||||
return new FallbackRateProvider(providers.ToArray());
|
||||
}
|
||||
|
||||
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
|
||||
{
|
||||
return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope };
|
||||
}
|
||||
|
||||
private IRateProvider GetDefaultRateProvider(BTCPayNetwork network)
|
||||
{
|
||||
if(network.DefaultRateProvider == null)
|
||||
var fallback = new FallbackRateProvider(providers.ToArray());
|
||||
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
|
||||
{
|
||||
throw new RateUnavailableException(network.CryptoCode);
|
||||
}
|
||||
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
|
||||
CacheSpan = CacheSpan
|
||||
};
|
||||
var value = await cached.GetRatesAsync();
|
||||
return new QueryRateResult()
|
||||
{
|
||||
CachedResult = !fallback.Used,
|
||||
ExchangeRates = value,
|
||||
Exceptions = fallback.Exceptions
|
||||
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,18 +5,13 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BitpayRateProviderDescription : RateProviderDescription
|
||||
{
|
||||
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
}
|
||||
}
|
||||
public class BitpayRateProvider : IRateProvider
|
||||
{
|
||||
public const string BitpayName = "bitpay";
|
||||
Bitpay _Bitpay;
|
||||
public BitpayRateProvider(Bitpay bitpay)
|
||||
{
|
||||
@ -24,21 +19,13 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new ArgumentNullException(nameof(bitpay));
|
||||
_Bitpay = bitpay;
|
||||
}
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false);
|
||||
var rate = rates.GetRate(currency);
|
||||
if (rate == 0m)
|
||||
throw new RateUnavailableException(currency);
|
||||
return (decimal)rate;
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return (await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
.AllRates
|
||||
.Select(r => new Rate() { Currency = r.Code, Value = r.Value })
|
||||
.ToList();
|
||||
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
|
||||
.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
@ -10,9 +11,8 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
private IRateProvider _Inner;
|
||||
private IMemoryCache _MemoryCache;
|
||||
private string _CryptoCode;
|
||||
|
||||
public CachedRateProvider(string cryptoCode, IRateProvider inner, IMemoryCache memoryCache)
|
||||
public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache)
|
||||
{
|
||||
if (inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new ArgumentNullException(nameof(memoryCache));
|
||||
this._Inner = inner;
|
||||
this.MemoryCache = memoryCache;
|
||||
this._CryptoCode = cryptoCode;
|
||||
this.ExchangeName = exchangeName;
|
||||
}
|
||||
|
||||
public IRateProvider Inner
|
||||
@ -31,31 +31,22 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public string ExchangeName { get; set; }
|
||||
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
|
||||
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRateAsync(currency);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public string AdditionalScope { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -21,29 +22,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public class CoinAverageRateProviderDescription : RateProviderDescription
|
||||
{
|
||||
public CoinAverageRateProviderDescription(string crypto)
|
||||
{
|
||||
CryptoCode = crypto;
|
||||
}
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new CoinAverageRateProvider(CryptoCode)
|
||||
{
|
||||
Authenticator = serviceProvider.GetService<ICoinAverageAuthenticator>()
|
||||
};
|
||||
}
|
||||
|
||||
IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return CreateRateProvider(serviceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetExchangeTickersResponse
|
||||
{
|
||||
public class Exchange
|
||||
@ -69,18 +47,18 @@ namespace BTCPayServer.Services.Rates
|
||||
public interface ICoinAverageAuthenticator
|
||||
{
|
||||
Task AddHeader(HttpRequestMessage message);
|
||||
}
|
||||
}
|
||||
|
||||
public class CoinAverageRateProvider : IRateProvider
|
||||
{
|
||||
public const string CoinAverageName = "coinaverage";
|
||||
public CoinAverageRateProvider()
|
||||
{
|
||||
|
||||
}
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
public CoinAverageRateProvider(string cryptoCode)
|
||||
{
|
||||
CryptoCode = cryptoCode ?? "BTC";
|
||||
}
|
||||
|
||||
public string Exchange { get; set; }
|
||||
public string Exchange { get; set; } = CoinAverageName;
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
@ -88,27 +66,19 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
get; set;
|
||||
} = "global";
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rates = await GetRatesCore();
|
||||
return GetRate(rates, currency);
|
||||
}
|
||||
|
||||
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
|
||||
{
|
||||
if (currency == "BTC")
|
||||
return 1.0m;
|
||||
if (rates.TryGetValue(currency, out decimal result))
|
||||
return result;
|
||||
throw new RateUnavailableException(currency);
|
||||
}
|
||||
|
||||
public ICoinAverageAuthenticator Authenticator { get; set; }
|
||||
|
||||
private async Task<Dictionary<string, decimal>> GetRatesCore()
|
||||
private bool TryToDecimal(JProperty p, out decimal v)
|
||||
{
|
||||
string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"];
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
var auth = Authenticator;
|
||||
@ -128,36 +98,29 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
if(Exchange != null)
|
||||
if (Exchange != CoinAverageName)
|
||||
{
|
||||
rates = (JObject)rates["symbols"];
|
||||
}
|
||||
return rates.Properties()
|
||||
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused))
|
||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
|
||||
{
|
||||
TryToDecimal(p, out decimal v);
|
||||
return v;
|
||||
});
|
||||
|
||||
var exchangeRates = new ExchangeRates();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
ExchangeRate exchangeRate = new ExchangeRate();
|
||||
exchangeRate.Exchange = Exchange;
|
||||
if (!TryToDecimal(prop, out decimal value))
|
||||
continue;
|
||||
exchangeRate.Value = value;
|
||||
if(CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
{
|
||||
exchangeRate.CurrencyPair = pair;
|
||||
exchangeRates.Add(exchangeRate);
|
||||
}
|
||||
}
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryToDecimal(JProperty p, out decimal v)
|
||||
{
|
||||
JToken token = p.Value[Exchange == null ? "last" : "bid"];
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
var rates = await GetRatesCore();
|
||||
return rates.Select(o => new Rate()
|
||||
{
|
||||
Currency = o.Key,
|
||||
Value = o.Value
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task TestAuthAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
|
||||
@ -217,7 +180,7 @@ namespace BTCPayServer.Services.Rates
|
||||
var exchanges = (JObject)jobj["exchanges"];
|
||||
response.Exchanges = exchanges
|
||||
.Properties()
|
||||
.Select(p =>
|
||||
.Select(p =>
|
||||
{
|
||||
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
|
||||
exchange.Name = p.Name;
|
||||
|
@ -20,12 +20,43 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Settings.AddHeader(message);
|
||||
}
|
||||
}
|
||||
|
||||
public class CoinAverageExchange
|
||||
{
|
||||
public CoinAverageExchange(string name, string display)
|
||||
{
|
||||
Name = name;
|
||||
Display = display;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Display { get; set; }
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
||||
{
|
||||
public CoinAverageExchanges()
|
||||
{
|
||||
Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
}
|
||||
|
||||
public void Add(CoinAverageExchange exchange)
|
||||
{
|
||||
Add(exchange.Name, exchange);
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
|
||||
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
|
||||
public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges();
|
||||
|
||||
public CoinAverageSettings()
|
||||
{
|
||||
@ -37,8 +68,9 @@ namespace BTCPayServer.Services.Rates
|
||||
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
|
||||
//}
|
||||
//b.AppendLine("}.ToArray()");
|
||||
|
||||
AvailableExchanges = new[] {
|
||||
AvailableExchanges = new CoinAverageExchanges();
|
||||
foreach(var item in
|
||||
new[] {
|
||||
(DisplayName: "BitBargain", Name: "bitbargain"),
|
||||
(DisplayName: "Tidex", Name: "tidex"),
|
||||
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
||||
@ -89,7 +121,10 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Quoine", Name: "quoine"),
|
||||
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
}.ToArray();
|
||||
})
|
||||
{
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
|
74
BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs
Normal file
74
BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ExchangeSharpRateProvider : IRateProvider
|
||||
{
|
||||
readonly ExchangeAPI _ExchangeAPI;
|
||||
readonly string _ExchangeName;
|
||||
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
|
||||
{
|
||||
if (exchangeAPI == null)
|
||||
throw new ArgumentNullException(nameof(exchangeAPI));
|
||||
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
|
||||
_ExchangeAPI = exchangeAPI;
|
||||
_ExchangeName = exchangeName;
|
||||
ReverseCurrencyPair = reverseCurrencyPair;
|
||||
}
|
||||
|
||||
public bool ReverseCurrencyPair
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = await _ExchangeAPI.GetTickersAsync();
|
||||
lock (notFoundSymbols)
|
||||
{
|
||||
var exchangeRates =
|
||||
rates.Select(t => CreateExchangeRate(t))
|
||||
.Where(t => t != null)
|
||||
.ToArray();
|
||||
return new ExchangeRates(exchangeRates);
|
||||
}
|
||||
}
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
HashSet<string> notFoundSymbols = new HashSet<string>();
|
||||
private ExchangeRate CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
|
||||
{
|
||||
if (notFoundSymbols.Contains(ticker.Key))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key);
|
||||
if (!CurrencyPair.TryParse(tickerName, out var pair))
|
||||
{
|
||||
notFoundSymbols.Add(ticker.Key);
|
||||
return null;
|
||||
}
|
||||
if(ReverseCurrencyPair)
|
||||
pair = new CurrencyPair(pair.Right, pair.Left);
|
||||
var rate = new ExchangeRate();
|
||||
rate.CurrencyPair = pair;
|
||||
rate.Exchange = _ExchangeName;
|
||||
rate.Value = ticker.Value.Bid;
|
||||
return rate;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
notFoundSymbols.Add(ticker.Key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,58 +2,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class FallbackRateProviderDescription : RateProviderDescription
|
||||
{
|
||||
public FallbackRateProviderDescription(RateProviderDescription[] rateProviders)
|
||||
{
|
||||
RateProviders = rateProviders;
|
||||
}
|
||||
|
||||
public RateProviderDescription[] RateProviders { get; set; }
|
||||
|
||||
public IRateProvider CreateRateProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new FallbackRateProvider(RateProviders.Select(r => r.CreateRateProvider(serviceProvider)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class FallbackRateProvider : IRateProvider
|
||||
{
|
||||
|
||||
IRateProvider[] _Providers;
|
||||
public bool Used { get; set; }
|
||||
public FallbackRateProvider(IRateProvider[] providers)
|
||||
{
|
||||
if (providers == null)
|
||||
throw new ArgumentNullException(nameof(providers));
|
||||
_Providers = providers;
|
||||
}
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
foreach(var p in _Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await p.GetRateAsync(currency).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
throw new RateUnavailableException(currency);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
Used = true;
|
||||
foreach (var p in _Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await p.GetRatesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
catch(Exception ex) { Exceptions.Add(ex); }
|
||||
}
|
||||
throw new RateUnavailableException("ALL");
|
||||
return new ExchangeRates();
|
||||
}
|
||||
|
||||
public List<Exception> Exceptions { get; set; } = new List<Exception>();
|
||||
}
|
||||
}
|
||||
|
@ -2,32 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class Rate
|
||||
{
|
||||
public Rate()
|
||||
{
|
||||
|
||||
}
|
||||
public Rate(string currency, decimal value)
|
||||
{
|
||||
Value = value;
|
||||
Currency = currency;
|
||||
}
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public decimal Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public interface IRateProvider
|
||||
{
|
||||
Task<decimal> GetRateAsync(string currency);
|
||||
Task<ICollection<Rate>> GetRatesAsync();
|
||||
Task<ExchangeRates> GetRatesAsync();
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class RateRules : IEnumerable<RateRule>
|
||||
{
|
||||
private List<RateRule> rateRules;
|
||||
|
||||
public RateRules()
|
||||
{
|
||||
rateRules = new List<RateRule>();
|
||||
}
|
||||
public RateRules(List<RateRule> rateRules)
|
||||
{
|
||||
this.rateRules = rateRules?.ToList() ?? new List<RateRule>();
|
||||
}
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IEnumerator<RateRule> GetEnumerator()
|
||||
{
|
||||
return rateRules.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
public interface IRateProviderFactory
|
||||
{
|
||||
IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules);
|
||||
TimeSpan CacheSpan { get; set; }
|
||||
void InvalidateCache();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class MockRateProviderFactory : IRateProviderFactory
|
||||
{
|
||||
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
|
||||
public MockRateProviderFactory()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TimeSpan CacheSpan { get; set; }
|
||||
|
||||
public void AddMock(MockRateProvider mock)
|
||||
{
|
||||
_Mocks.Add(mock);
|
||||
}
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
|
||||
{
|
||||
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
List<Rate> _Rates;
|
||||
|
||||
public string CryptoCode { get; }
|
||||
|
||||
public MockRateProvider(string cryptoCode, params Rate[] rates)
|
||||
{
|
||||
_Rates = new List<Rate>(rates);
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public MockRateProvider(string cryptoCode, List<Rate> rates)
|
||||
{
|
||||
_Rates = rates;
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase));
|
||||
if (rate == null)
|
||||
throw new RateUnavailableException(currency);
|
||||
return Task.FromResult(rate.Value);
|
||||
}
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
ICollection<Rate> rates = _Rates;
|
||||
return Task.FromResult(rates);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,32 +4,15 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class QuadrigacxRateProvider : IRateProvider
|
||||
{
|
||||
public QuadrigacxRateProvider(string crypto)
|
||||
{
|
||||
CryptoCode = crypto;
|
||||
}
|
||||
public string CryptoCode { get; set; }
|
||||
public const string QuadrigacxName = "quadrigacx";
|
||||
static HttpClient _Client = new HttpClient();
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return await GetRatesAsyncCore(CryptoCode, currency);
|
||||
}
|
||||
|
||||
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
if (!TryToDecimal(rates, out var result))
|
||||
throw new RateUnavailableException(currency);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryToDecimal(JObject p, out decimal v)
|
||||
{
|
||||
@ -40,26 +23,26 @@ namespace BTCPayServer.Services.Rates
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
List<Rate> result = new List<Rate>();
|
||||
var exchangeRates = new ExchangeRates();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
var rate = new Rate();
|
||||
var splitted = prop.Name.Split('_');
|
||||
var crypto = splitted[0].ToUpperInvariant();
|
||||
if (crypto != CryptoCode)
|
||||
var rate = new ExchangeRate();
|
||||
if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
continue;
|
||||
rate.CurrencyPair = pair;
|
||||
rate.Exchange = QuadrigacxName;
|
||||
if (!TryToDecimal((JObject)prop.Value, out var v))
|
||||
continue;
|
||||
rate.Currency = splitted[1].ToUpperInvariant();
|
||||
TryToDecimal((JObject)prop.Value, out var v);
|
||||
rate.Value = v;
|
||||
result.Add(rate);
|
||||
exchangeRates.Add(rate);
|
||||
}
|
||||
return result;
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface RateProviderDescription
|
||||
{
|
||||
IRateProvider CreateRateProvider(IServiceProvider serviceProvider);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class RateUnavailableException : Exception
|
||||
{
|
||||
public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency)
|
||||
{
|
||||
if (currency == null)
|
||||
throw new ArgumentNullException(nameof(currency));
|
||||
Currency = currency;
|
||||
}
|
||||
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class TweakRateProvider : IRateProvider
|
||||
{
|
||||
private BTCPayNetwork network;
|
||||
private IRateProvider rateProvider;
|
||||
private RateRules rateRules;
|
||||
|
||||
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (rateProvider == null)
|
||||
throw new ArgumentNullException(nameof(rateProvider));
|
||||
if (rateRules == null)
|
||||
throw new ArgumentNullException(nameof(rateRules));
|
||||
this.network = network;
|
||||
this.rateProvider = rateProvider;
|
||||
this.rateRules = rateRules;
|
||||
}
|
||||
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rate = await rateProvider.GetRateAsync(currency);
|
||||
foreach(var rule in rateRules)
|
||||
{
|
||||
rate = rule.Apply(network, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
List<Rate> rates = new List<Rate>();
|
||||
foreach (var rate in await rateProvider.GetRatesAsync())
|
||||
{
|
||||
var localRate = rate.Value;
|
||||
foreach (var rule in rateRules)
|
||||
{
|
||||
localRate = rule.Apply(network, localRate);
|
||||
}
|
||||
rates.Add(new Rate(rate.Currency, localRate));
|
||||
}
|
||||
return rates;
|
||||
}
|
||||
}
|
||||
}
|
@ -44,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();
|
||||
}
|
||||
@ -84,7 +86,9 @@ namespace BTCPayServer.Services.Stores
|
||||
.ToArrayAsync())
|
||||
.Select(u =>
|
||||
{
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
u.StoreData.Role = u.Role;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
return u.StoreData;
|
||||
}).ToArray();
|
||||
}
|
||||
|
@ -5,11 +5,6 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class StorePolicies
|
||||
{
|
||||
public const string CanAccessStores = "CanAccessStore";
|
||||
public const string OwnStore = "OwnStore";
|
||||
}
|
||||
public class StoreRoles
|
||||
{
|
||||
public const string Owner = "Owner";
|
||||
|
@ -66,9 +66,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-item-order__right">
|
||||
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
|
||||
<div class="single-item-order__right__btc-price" v-if="srvModel.status === 'paid'">
|
||||
<span>{{ srvModel.btcPaid }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
<div class="single-item-order__right__btc-price" v-else>
|
||||
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
|
||||
<div class="single-item-order__right__ex-rate">
|
||||
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
|
||||
</div>
|
||||
|
@ -11,12 +11,12 @@
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>BTCPay Invoice</title>
|
||||
<title>@Model.HtmlTitle</title>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
||||
|
@ -11,6 +11,7 @@
|
||||
.smMaxWidth {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
@@media (min-width: 768px) {
|
||||
.smMaxWidth {
|
||||
max-width: 400px;
|
||||
@ -166,10 +167,14 @@
|
||||
<th class="text-right">Rate</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Due</th>
|
||||
@if(Model.StatusException == "paidOver")
|
||||
{
|
||||
<th class="text-right">Overpaid</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.CryptoPayments)
|
||||
@foreach(var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
@ -177,13 +182,17 @@
|
||||
<td class="text-right">@payment.Rate</td>
|
||||
<td class="text-right">@payment.Paid</td>
|
||||
<td class="text-right">@payment.Due</td>
|
||||
@if(Model.StatusException == "paidOver")
|
||||
{
|
||||
<td class="text-right">@payment.Overpaid</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.OnChainPayments.Count > 0)
|
||||
@if(Model.OnChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@ -198,7 +207,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.OnChainPayments)
|
||||
@foreach(var payment in Model.OnChainPayments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "class='linethrough'" : "";
|
||||
<tr @replaced>
|
||||
@ -217,7 +226,7 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.OffChainPayments.Count > 0)
|
||||
@if(Model.OffChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@ -230,7 +239,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.OffChainPayments)
|
||||
@foreach(var payment in Model.OffChainPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.Crypto</td>
|
||||
@ -253,7 +262,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var address in Model.Addresses)
|
||||
@foreach(var address in Model.Addresses)
|
||||
{
|
||||
var current = address.Current ? "font-weight-bold" : "";
|
||||
<tr>
|
||||
@ -277,7 +286,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var evt in Model.Events)
|
||||
@foreach(var evt in Model.Events)
|
||||
{
|
||||
<tr>
|
||||
<td>@evt.Timestamp</td>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<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 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
|
||||
</p>
|
||||
<ul>
|
||||
|
@ -22,6 +22,7 @@
|
||||
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Html.Raw(Model.AuthenticatorUri)"></div>
|
||||
<br />
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
|
@ -53,6 +53,7 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="TestEmail"></label>
|
||||
<input asp-for="TestEmail" class="form-control" />
|
||||
<span asp-validation-for="TestEmail" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@model RatesViewModel
|
||||
@model BTCPayServer.Models.ServerViewModels.RatesViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Rates);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<form method="post">
|
||||
<button type="submit" class="btn btn-secondary btn-danger" title="Continue">@Model.Action</button>
|
||||
<button type="submit" class="btn btn-secondary @Model.ButtonClass" title="Continue">@Model.Action</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -125,7 +125,7 @@
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
<script type="text/javascript">
|
||||
@Model.ServerUrl.ToJSVariableModel("srvModel");
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model.ServerUrl));
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
|
||||
|
@ -26,6 +26,11 @@
|
||||
<input asp-for="CustomCSS" class="form-control" />
|
||||
<span asp-validation-for="CustomCSS" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="HtmlTitle"></label>
|
||||
<input asp-for="HtmlTitle" class="form-control" />
|
||||
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultCryptoCurrency"></label>
|
||||
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
|
149
BTCPayServer/Views/Stores/Rates.cshtml
Normal file
149
BTCPayServer/Views/Stores/Rates.cshtml
Normal file
@ -0,0 +1,149 @@
|
||||
@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></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Script"></label>
|
||||
<textarea asp-for="Script" rows="20" cols="80" class="form-control"></textarea>
|
||||
<span asp-validation-for="Script" class="text-danger"></span>
|
||||
<a href="#" onclick="$('#Script').val(defaultScript); return false;">Set to default settings</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a asp-action="ShowRateRules" asp-route-scripting="false">Turn off advanced rate rule scripting</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a asp-action="ShowRateRules" asp-route-scripting="true">Turn on advanced rate rule scripting</a>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="RateMultiplier"></label>
|
||||
<input asp-for="RateMultiplier" class="form-control" />
|
||||
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h5>Testing</h5>
|
||||
<span>Enter currency pairs which you want to test against your rule (eg. <code>DOGE_USD,DOGE_CAD,BTC_CAD,BTC_USD</code>)</span>
|
||||
<div class="input-group">
|
||||
<input placeholder="BTC_USD, BTC_CAD" asp-for="ScriptTest" class="form-control" />
|
||||
<span class="input-group-btn">
|
||||
<button name="command" value="Test" type="submit" class="btn btn-primary" title="Test">
|
||||
<span class="fa fa-vial"></span> Test
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<span asp-validation-for="ScriptTest" class="text-danger"></span>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
<input type="hidden" asp-for="ShowScripting" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script type="text/javascript">var defaultScript = @Html.Raw(Json.Serialize(Model.DefaultScript));</script>
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Stores
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
public static string Index => "Index";
|
||||
public static string Rates => "Rates";
|
||||
public static string Checkout => "Checkout experience";
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
@ -20,6 +21,7 @@ namespace BTCPayServer.Views.Stores
|
||||
|
||||
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
|
@ -34,19 +34,6 @@
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RateMultiplier"></label>
|
||||
<input asp-for="RateMultiplier" class="form-control" />
|
||||
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="InvoiceExpiration"></label>
|
||||
<input asp-for="InvoiceExpiration" class="form-control" />
|
||||
|
@ -65,7 +65,7 @@
|
||||
@section Scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @StoreNavPages.IndexNavClass(ViewContext)" asp-action="UpdateStore">General settings</a>
|
||||
<a class="nav-link @StoreNavPages.RatesNavClass(ViewContext)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @StoreNavPages.CheckoutNavClass(ViewContext)" asp-action="CheckoutExperience">Checkout experience</a>
|
||||
<a class="nav-link @StoreNavPages.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
|
||||
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
|
||||
|
1
BTCPayServer/wwwroot/js/qrcode.min.js
vendored
Normal file
1
BTCPayServer/wwwroot/js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user