Compare commits

...

137 Commits

Author SHA1 Message Date
4f9e4116a2 Point of Sale support free entry 2018-04-26 22:09:18 +09:00
82d8fda05f update clightning in tests 2018-04-26 15:30:52 +09:00
d4935263da Update various packages 2018-04-26 11:45:09 +09:00
e158d909fb bump 2018-04-26 11:16:56 +09:00
de8147d5dd Can opt out required refund email from customer 2018-04-26 11:13:44 +09:00
16f1791a9a Invoice filter must work with duplicated filter 2018-04-26 11:03:02 +09:00
8745c3f8c6 Merge pull request from lepipele/dev-timerfix
Recoding timer removing dependecy on browser's setInterval
2018-04-26 09:18:04 +09:00
ec5b45cff6 Recoding timer removing dependecy on browser's setInterval
Ref: https://github.com/btcpayserver/btcpayserver/issues/130
2018-04-25 13:30:00 -05:00
1348197295 Merge pull request from lepipele/master
Ellipsis when there is lots of info, preserving responsive tables
2018-04-24 12:00:54 +09:00
f2516854d8 Fixing width to align first columns
Ref: https://github.com/btcpayserver/btcpayserver/pull/134#issuecomment-383785811
2018-04-23 21:58:22 -05:00
062ca6e743 Merge remote-tracking branch 'source/master' 2018-04-23 21:53:06 -05:00
44b6997bb5 Merge pull request from Saevar2000/patch-1
Update is.js
2018-04-24 11:39:11 +09:00
78b544f9ca Update is.js 2018-04-23 23:08:35 +00:00
81926b4450 Ellipsis when there is lots of info, preserving responsive tables
Ref: https://github.com/btcpayserver/btcpayserver/issues/133
2018-04-23 16:00:03 -05:00
a7ad71d492 CoinAverage credentials are now correctly passed 2018-04-23 17:21:50 +09:00
18977f7265 Optimize number of requests sent to Quadrigacx 2018-04-23 17:06:22 +09:00
8a88b44e98 Add special rate provider for qudrigacx 2018-04-23 16:44:59 +09:00
c9e5fe42ba Set default AvailableExchanges inside CoinAverageSettings 2018-04-23 16:12:11 +09:00
56dffbf514 Set default exchange list 2018-04-23 16:09:18 +09:00
0e1fac3773 fix getting exchange rate of Coinaverage 2018-04-23 15:58:35 +09:00
e7c06880a8 Use API keys of bitcoinaverage for getting the exchange list 2018-04-23 15:48:18 +09:00
39463a3202 Merge pull request from lepipele/master
Removing empty folder, fixing build warnings
2018-04-23 12:39:10 +09:00
36136f0f0f Removing empty folder, fixing build warnings 2018-04-22 22:30:37 -05:00
22e5b2869a bump 2018-04-20 12:28:58 +09:00
fc3f32a4e0 Merge branch 'dev-bootstrap' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap 2018-04-20 12:17:53 +09:00
3b0914e89e Migrating ManageNavPages to new navigation enum 2018-04-19 15:57:23 -05:00
76cd9a7b25 Abstracting navigation so it can use any enums 2018-04-19 15:42:12 -05:00
0934bebf7b Merge remote-tracking branch 'source/master' into dev-bootstrap 2018-04-19 11:45:30 -05:00
cd1a4c4749 Fixing modify user page and it's title 2018-04-19 11:44:24 -05:00
8075273ec8 Refactoring pills navigation 2018-04-19 11:40:12 -05:00
97b59be9bf Adding page for Theme settings 2018-04-19 11:39:51 -05:00
b87ec4f3d9 Primitive versioning of css files to ensure update on change 2018-04-19 11:15:45 -05:00
3822358096 Show more info about bitcoin average quota 2018-04-20 01:01:39 +09:00
ba7e8cfe78 Removing Merriweather as default body font, back to Arial
Ref: https://forkbitpay.slack.com/archives/C6PSCRFAM/p1524130675000104
2018-04-19 10:04:59 -05:00
41978f1c59 Remove useless line in invoice.cshtml 2018-04-19 18:39:39 +09:00
e75e691404 Merge branch 'dev-bootstrap' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap 2018-04-19 18:03:04 +09:00
a22216fd04 fix layout 2018-04-19 17:06:08 +09:00
6900e16aa4 bump 2018-04-19 16:54:47 +09:00
10c981b2a0 Update NBXplorer 2018-04-19 16:54:25 +09:00
5f940df1b4 Migrating Invoice styling 2018-04-18 23:44:01 -05:00
3f85918a0c Merge remote-tracking branch 'source/master' into dev-bootstrap
# Conflicts:
#	BTCPayServer/Controllers/ServerController.cs
#	BTCPayServer/Views/Invoice/Invoice.cshtml
2018-04-18 23:38:10 -05:00
e4299c09ea bump 2018-04-18 22:28:31 +09:00
e864cf35f7 bump NBitcoin 2018-04-18 22:28:04 +09:00
3652866660 View offchain payments in Invoice screen 2018-04-18 22:27:01 +09:00
0421004616 fix point of sale view on mobile 2018-04-18 21:52:13 +09:00
6936b034cb Add Bitcoin average quota 2018-04-18 18:23:39 +09:00
73ed4003a3 Use a drop down for preferred exchange list 2018-04-18 16:38:56 +09:00
5cb8cdd511 Refactoring: Do not query database when asking for Coinaverage rates, periodically get exchange list 2018-04-18 16:38:56 +09:00
195b5fdd1a Adding overriding of CreativeStartUri, refactoring PoliciesSettings 2018-04-17 17:24:00 -05:00
d19b78b6cc Moving Creative Start files to dedicated folder 2018-04-17 17:23:33 -05:00
9bbc05c3a7 Cleaning Invoice table, removing style attrs
Ref: https://github.com/btcpayserver/btcpayserver/issues/82

Co-authored-by: Esky33 <support@btcpayjungle.com>
2018-04-17 16:48:50 -05:00
7df3c86649 Tweaking primary color now that creative.css no longer overrides 2018-04-17 16:29:05 -05:00
c6e0a923bb Unifying bg-dark style, cleaning up references to extra colors 2018-04-17 16:22:20 -05:00
637fe1727b Adding missing font styles back in
These are referenced by Creative - Start Bootstrap theme
2018-04-17 16:12:17 -05:00
fd087bbeb8 Streamlining style for footer 2018-04-17 15:33:29 -05:00
2f515e1cc0 Removing unused classes 2018-04-17 15:20:27 -05:00
84cd9e570f Merge pull request from pajasevi/cs-trancaction-count
Transaction count CS translation
2018-04-17 11:31:29 +09:00
ead97a24bd Update NBitcoin 2018-04-16 19:15:44 +09:00
415cde1629 Transaction count CS translation 2018-04-16 09:11:46 +02:00
b438312fde fix js 2018-04-16 11:38:10 +09:00
79ff2cb271 Merge pull request from bitmario/master
Add Portuguese (Portugal) translation
2018-04-16 11:28:54 +09:00
ed1464c405 Merge pull request from LinoxBE/dutch-translation
Dutch update txCount
2018-04-16 11:28:10 +09:00
f85631429b Merge pull request from mutedstorm/patch-2
fix german translation
2018-04-16 11:27:41 +09:00
5ed56d1137 Update JA translations 2018-04-16 11:26:29 +09:00
d7719d25b4 Add Portuguese (Portugal) translation 2018-04-16 01:29:42 +01:00
6267cccc3f fix german translation
minor changes, thanks to (@raindogdance)
2018-04-15 22:47:08 +02:00
fd5c4021f7 Dutch update txCount 2018-04-15 20:00:11 +02:00
b8bf4d99ac Bump 2018-04-15 21:29:44 +09:00
0723eec508 Fix rate handling 2018-04-15 21:21:57 +09:00
7f01a12245 Merge pull request from mutedstorm/patch-1
fix german translation
2018-04-15 21:20:06 +09:00
e1e3e5d953 fix german translation
fixed small errors and changed "Geldbörse / Brieftasche" back to Wallet because its never translated on German sites so its unnecessary.
2018-04-15 12:32:24 +02:00
18986faca8 Merge remote-tracking branch 'source/master' into dev-bootstrap
# Conflicts:
#	BTCPayServer/Controllers/ServerController.cs
2018-04-14 11:11:38 -05:00
2a68f8a90f Merge pull request from lepipele/master
Adding German translations
2018-04-14 10:54:04 -05:00
659936577b Adding German translations
Again using Google Translate, we need native speaker to review them
2018-04-14 10:53:02 -05:00
85efc3b00c fix tests 2018-04-14 23:32:39 +09:00
5efac45d46 bump 2018-04-14 22:55:35 +09:00
c7dce280d7 fix js 2018-04-14 22:53:31 +09:00
04c6107196 Can configure rate caching and bitcoinaverage API keys 2018-04-14 22:52:57 +09:00
54ce9b5dab Merge pull request from rsandrade/patch-2
Update pt_BR.js
2018-04-14 21:40:38 +09:00
cee955fb9d Update pt_BR.js 2018-04-14 07:48:31 -03:00
2e4b0daa48 add french translation, bump NBitcoin 2018-04-14 13:18:56 +09:00
e85ccfb47e Merge pull request from lepipele/master
Improvements to i18n, invoice expiry bugfix
2018-04-14 13:06:45 +09:00
b099f93c78 Adjusting Policies form to look better on different screen sizes 2018-04-13 16:15:21 -05:00
81afe397be CssThemeManager that injects Bootstrap css uri from settings 2018-04-13 16:15:03 -05:00
f869c06aee Adding Bootstrap theme uri field to settings 2018-04-13 15:42:34 -05:00
75099b99d4 TxCount strings in Spanish 2018-04-13 14:44:42 -05:00
7b1b2a0f68 Bugfixing redirect link when invoice expires
Refactoring logic so that it's same for paid and expired
2018-04-13 14:39:45 -05:00
203c28df3d Extracting transaction string and supporting plural form 2018-04-13 14:10:06 -05:00
2e2c3cdec4 bump 2018-04-14 00:06:00 +09:00
6f827c86a4 Update images and bump 2018-04-13 14:34:29 +09:00
5aced90a3f Merge pull request from iamvinny/master
Fix Portuguese translation
2018-04-13 10:47:37 +09:00
4646f88e3a Fix Portuguese translation 2018-04-12 18:45:05 -03:00
2b11cc1077 Simplify root key path calculation 2018-04-12 11:48:33 +09:00
77b42eb085 Do not forget to pass expiry to createinvoice on clightning 2018-04-11 18:42:19 +09:00
7de067cd7a remove unused 2018-04-10 19:12:37 +09:00
9da6df50b7 Add DOGECOIN 2018-04-10 19:07:57 +09:00
66b1623109 Merge pull request from lepipele/master
Fixing ForgotPassword, updating BundleMinifier
2018-04-10 13:22:36 +09:00
2432834f3d Updating BundleMinifier, now supporting CSS variables 2018-04-09 23:13:14 -05:00
01fa483f95 Improving styling of Forgot password page
Fixes: https://github.com/btcpayserver/btcpayserver/issues/108
2018-04-09 23:12:03 -05:00
1ddf47256f Show more invoices on the invoice page, better search button 2018-04-09 17:53:43 +09:00
25fe32c3f8 Add border to table 2018-04-09 17:43:33 +09:00
ac9b8d03d7 Fix slow invoice creation 2018-04-09 16:25:31 +09:00
8fdfb2c4f6 Fix Back to Website path for Hangfire 2018-04-09 14:41:52 +09:00
b1da136f77 Update packages and remove hangfire hack 2018-04-09 14:31:39 +09:00
9a6f85fa21 Prevent full crash if lightning crash 2018-04-09 10:48:16 +09:00
7308453a74 Merge branch 'lepipele-dev-bootstrap4' 2018-04-08 14:57:54 +09:00
b798a17ef8 Updating Bootstrap4 path on POS 2018-04-08 00:28:39 -05:00
4b8899860e Migrating btn-info to btn-secondary 2018-04-08 00:25:00 -05:00
f46c8a0a0f Migrating btn-success to btn-primary 2018-04-08 00:08:15 -05:00
48832f9ac3 Updating tables not to have top border and margin as requested 2018-04-08 00:06:47 -05:00
9c798fc2e2 Bootstrap 4 custom theme to address issues from
Including SCSS variable changes in case we eventually want to
generate results CSS from Bootstrap SCSS
2018-04-08 00:05:00 -05:00
4704587f0a Removing unused Bootstrap 4 flavors and versions 2018-04-07 23:53:08 -05:00
58e6b63fd7 Removing legacy btn-default style 2018-04-07 23:50:34 -05:00
3c76dfb584 Migrating to btn-primary
btn-default has been removed in Bootstrap4:
https://github.com/twbs/bootstrap/issues/25029
2018-04-07 23:49:36 -05:00
10055d987d Merge remote-tracking branch 'source/master' into dev-bootstrap4 2018-04-07 23:22:11 -05:00
be49c60e83 Update lightning-charge 2018-04-08 12:29:20 +09:00
14016e2f84 Fix grammar 2018-04-07 21:34:24 +09:00
d7cb6f1cca Add a way to customize lightning invoice description 2018-04-07 16:27:46 +09:00
0f63162254 Merge branch 'dev-bootstrap4' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap4 2018-04-07 12:05:26 +09:00
21215dc537 Make sure a too high expiration do not trigger "The value needs to translate in milliseconds to -1 (signifying an infinite timeout)" 2018-04-07 11:53:33 +09:00
20e147edfc Fix 2018-04-07 02:49:26 +09:00
1048dd516b Pass itemDesc to lightning invoice (Fix ) 2018-04-07 02:43:35 +09:00
42f44327f0 Update NBitcoin and NBXplorer 2018-04-07 02:23:12 +09:00
49200a4a9c Removed old Bootstrap, no longer needed 2018-04-06 00:16:27 -05:00
7d71757de3 Merge remote-tracking branch 'source/master' into dev-bootstrap4 2018-04-06 00:14:18 -05:00
0fb492a70f Migrating to FontAwesome
Glyphicons dropped from Bootstrap4:
https://getbootstrap.com/docs/4.0/migration/#components
New version of Glyphicons not readily available in CSS format
Using FA since it's already in solution
2018-04-06 00:14:07 -05:00
7ccc1abb95 Moving checkout CSS and JS to dedicated folder 2018-04-05 23:56:17 -05:00
d61858e260 Cleaning up CSS and JS files used for main theme 2018-04-05 23:51:55 -05:00
0ecd40f299 Removing legacy css files no longer used 2018-04-05 23:33:43 -05:00
d9d4e74126 Preserving btn-default style that's removed from Bootstrap4 2018-04-05 23:31:53 -05:00
42d04bff61 Migrating table styles 2018-04-05 23:20:12 -05:00
f9cc29f014 Removing CSS variables until NUglify is merged for bundling
Ref: https://github.com/madskristensen/BundlerMinifier/issues/306
2018-04-05 22:56:18 -05:00
992d359e79 Add a --rootpath option 2018-04-05 15:50:23 +09:00
1cc5427cbb Improve error message if you can't create an invoice in the UI 2018-04-05 15:44:27 +09:00
40092b60fa Migrating navigation pills 2018-04-03 23:30:28 -05:00
5356b74490 Switching bundling to point to Bootstrap 4 2018-04-03 23:30:19 -05:00
e832ce5b4a Adding Bootstrap 4 to solution 2018-04-03 23:29:59 -05:00
177 changed files with 19087 additions and 29035 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csproj
Configuration
Controllers
Data
DerivationSchemeParser.csExtensions.cs
HostedServices
Hosting
Models
MultiValueDictionary.cs
Payments
SearchString.cs
Services
SynchronizationContextRemover.cs
Views
bundleconfig.json
wwwroot

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

@ -47,7 +47,7 @@ namespace BTCPayServer.Tests
}
public Uri LTCNBXplorerUri { get; set; }
public Uri ServerUri
{
get;
@ -65,11 +65,14 @@ namespace BTCPayServer.Tests
get; set;
}
public bool MockRates { get; set; } = true;
public void Start()
{
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
string chain = ChainType.Regtest.ToNetwork().Name;
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
string chainDirectory = Path.Combine(_Directory, chain);
if (!Directory.Exists(chainDirectory))
Directory.CreateDirectory(chainDirectory);
@ -101,12 +104,15 @@ namespace BTCPayServer.Tests
.UseConfiguration(conf)
.ConfigureServices(s =>
{
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);
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)
@ -120,9 +126,8 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
}
public string HostName
{
get;

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
@ -9,4 +9,4 @@ RUN dotnet restore
# copies the rest of your code
COPY . ../.
ENTRYPOINT ["dotnet", "test"]
ENTRYPOINT ["dotnet", "test"]

@ -34,22 +34,12 @@ namespace BTCPayServer.Tests
public ServerTester(string scope)
{
_Directory = scope;
}
public bool Dockerized
{
get; set;
}
public void Start()
{
if (Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory);
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
@ -72,6 +62,15 @@ namespace BTCPayServer.Tests
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
}
public bool Dockerized
{
get; set;
}
public void Start()
{
PayTester.Start();
}

@ -132,7 +132,8 @@ namespace BTCPayServer.Tests
{
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString())
: throw new NotSupportedException(connectionType.ToString()),
SkipPortTest = true
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);

@ -598,8 +598,18 @@ namespace BTCPayServer.Tests
var search = new SearchString(filter);
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch);
Assert.Equal("abc", search.Filters["storeid"]);
Assert.Equal("abed", search.Filters["status"]);
Assert.Single(search.Filters["storeid"]);
Assert.Single(search.Filters["status"]);
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
filter = "status:abed status:abed2";
search = new SearchString(filter);
Assert.Equal("status:abed status:abed2", search.ToString());
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
Assert.Equal(2, search.Filters["status"].Count);
Assert.Equal("abed", search.Filters["status"].First());
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
}
[Fact]
@ -616,12 +626,54 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanUseExchangeSpecificRate()
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer);
foreach (var rate in rates)
{
Assert.Single(rates.Where(r => r == rate));
}
}
}
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;
vm.PreferredExchange = exchange;
storeController.UpdateStore(user.StoreId, vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
return invoice2.CryptoInfo[0].Rate;
}
[Fact]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
@ -831,7 +883,7 @@ namespace BTCPayServer.Tests
[Fact]
public void CanParseDerivationScheme()
{
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
var parser = new DerivationSchemeParser(Network.TestNet);
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
// Passing electrum stuff
// Native
@ -945,7 +997,7 @@ namespace BTCPayServer.Tests
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, "orange").Result);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
@ -1012,13 +1064,13 @@ namespace BTCPayServer.Tests
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
StoreId = new[] { user.StoreId },
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
StoreId = new[] { user.StoreId },
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
@ -1143,6 +1195,26 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider("BTC");
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());
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 CheckRatesProvider()
{
@ -1164,7 +1236,7 @@ namespace BTCPayServer.Tests
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}

@ -46,7 +46,7 @@ services:
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.24
image: nicolasdorier/nbxplorer:1.0.2.2
ports:
- "32838:32838"
expose:
@ -89,7 +89,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning
image: nicolasdorier/clightning:0.0.0.9-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -98,6 +98,7 @@ services:
network=regtest
ipaddr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
ports:
- "30992:9835" # api port
expose:
@ -110,11 +111,10 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.5
image: shesek/lightning-charge:0.3.9
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
SKIP_BITCOIND: 1
BITCOIND_RPCCONNECT: bitcoind
volumes:
- "bitcoin_datadir:/etc/bitcoin"
@ -130,7 +130,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning
image: nicolasdorier/clightning:0.0.0.9-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -139,6 +139,7 @@ services:
ipaddr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000
ports:
- "30993:9835" # api port
expose:

@ -14,34 +14,28 @@ namespace BTCPayServer
{
static BTCPayDefaultSettings()
{
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
{
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
chainType == ChainType.Regtest ? Network.RegTest :
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
var settings = new BTCPayDefaultSettings();
_Settings.Add(chainType, settings);
settings.ChainType = chainType;
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
chainType == ChainType.Regtest ? 23002 :
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
chainType == NetworkType.Regtest ? 23002 :
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
}
}
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
{
return _Settings[chainType];
}
public string DefaultDataDirectory { get; set; }
public string DefaultConfigurationFile { get; set; }
public ChainType ChainType { get; internal set; }
public int DefaultPort { get; set; }
}
public class BTCPayNetwork
@ -50,7 +44,7 @@ namespace BTCPayServer
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
public IRateProvider DefaultRateProvider { get; set; }
public RateProviderDescription DefaultRateProvider { get; set; }
[Obsolete("Should not be needed")]
public bool IsBTC
@ -72,6 +66,12 @@ namespace BTCPayServer
public override string ToString()
{
return CryptoCode;
}
}
internal KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
}
}

@ -14,21 +14,21 @@ namespace BTCPayServer
public void InitBitcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
var coinaverage = new CoinAverageRateProvider("BTC");
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
var coinaverage = new CoinAverageRateProviderDescription("BTC");
var bitpay = new BitpayRateProviderDescription();
var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay });
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDogecoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}
}
}

@ -12,22 +12,19 @@ namespace BTCPayServer
{
public void InitLitecoin()
{
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
var ltcRate = new CoinAverageRateProvider("LTC");
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
});
}
}

@ -27,8 +27,8 @@ namespace BTCPayServer
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
{
ChainType = filtered.ChainType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.ChainType);
NetworkType = filtered.NetworkType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
_Networks = new Dictionary<string, BTCPayNetwork>();
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
foreach (var network in filtered._Networks)
@ -40,13 +40,14 @@ namespace BTCPayServer
}
}
public ChainType ChainType { get; set; }
public BTCPayNetworkProvider(ChainType chainType)
public NetworkType NetworkType { get; private set; }
public BTCPayNetworkProvider(NetworkType networkType)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
ChainType = chainType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;
InitBitcoin();
InitLitecoin();
InitDogecoin();
}
/// <summary>

@ -2,21 +2,25 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.73</Version>
<Version>1.0.1.93</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\css\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\dockerfiles\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\css\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\dockerfiles\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\css\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\dockerfiles\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\css\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
</ItemGroup>
<ItemGroup>
@ -26,19 +30,19 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.362" />
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<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.0.0.65" />
<PackageReference Include="NBitcoin" Version="4.1.1.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.16" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
@ -49,8 +53,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
</ItemGroup>
@ -61,13 +65,8 @@
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\js\checkout\core.js" />
<None Include="wwwroot\js\creative.js" />
<None Include="wwwroot\js\creative.min.js" />
<None Include="wwwroot\js\site.js" />
<None Include="wwwroot\js\site.min.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
<None Include="wwwroot\checkout\js\core.js" />
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />

@ -23,7 +23,7 @@ namespace BTCPayServer.Configuration
public class BTCPayServerOptions
{
public ChainType ChainType
public NetworkType NetworkType
{
get; set;
}
@ -51,15 +51,15 @@ namespace BTCPayServer.Configuration
public void LoadArgs(IConfiguration conf)
{
ChainType = DefaultConfiguration.GetChainType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
NetworkType = DefaultConfiguration.GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (NetworkProvider.GetNetwork(chain) == null)
@ -92,11 +92,15 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
RootPath = conf.GetOrDefault<string>("rootpath", "/");
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if(old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public BTCPayNetworkProvider NetworkProvider { get; set; }
@ -115,5 +119,14 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
}
}

@ -18,7 +18,7 @@ namespace BTCPayServer.Configuration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
var provider = new BTCPayNetworkProvider(ChainType.Main);
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
CommandLineApplication app = new CommandLineApplication(true)
{
@ -31,6 +31,9 @@ namespace BTCPayServer.Configuration
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -38,8 +41,6 @@ namespace BTCPayServer.Configuration
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
}
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
return app;
}
@ -47,12 +48,12 @@ namespace BTCPayServer.Configuration
protected override string GetDefaultDataDir(IConfiguration conf)
{
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
var dataDir = conf["datadir"];
if (dataDir == null)
return network.DefaultConfigurationFile;
@ -68,7 +69,7 @@ namespace BTCPayServer.Configuration
return Path.Combine(chainDir, fileName);
}
public static ChainType GetChainType(IConfiguration conf)
public static NetworkType GetNetworkType(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if (network != null)
@ -78,17 +79,18 @@ namespace BTCPayServer.Configuration
{
throw new ConfigException($"Invalid network parameter '{network}'");
}
return n.ToChainType();
return n.NetworkType;
}
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
return net;
}
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
var networkType = GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#network=mainnet");
@ -101,7 +103,7 @@ namespace BTCPayServer.Configuration
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
{
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
@ -115,7 +117,7 @@ namespace BTCPayServer.Configuration
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
}
}
}

@ -42,10 +42,12 @@ namespace BTCPayServer.Controllers
" price: 15\n\n" +
"tshirt:\n" +
" price: 25";
ShowCustomAmount = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool ShowCustomAmount { get; set; }
}
[HttpGet]
@ -57,7 +59,7 @@ namespace BTCPayServer.Controllers
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, Currency = settings.Currency, Template = settings.Template });
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
}
[HttpPost]
[Route("{appId}/settings/pos")]
@ -83,6 +85,7 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
ShowCustomAmount = vm.ShowCustomAmount,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template
});
@ -99,9 +102,13 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = Parse(settings.Template, settings.Currency)
});
}
@ -155,23 +162,43 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, string choiceKey)
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
double price = 0.0;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = (double)choice.Price.Value;
}
else
{
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = choice.Title,
ItemDesc = title,
Currency = settings.Currency,
Price = (double)choice.Price.Value,
Price = price,
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}

@ -87,8 +87,8 @@ namespace BTCPayServer.Controllers
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
Status = status == null ? null : new[] { status },
StoreId = new[] { store.Id }
};

@ -85,54 +85,66 @@ namespace BTCPayServer.Controllers
model.CryptoPayments.Add(cryptoPayment);
}
var payments = invoice
var onChainPayments = invoice
.GetPayments()
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(async payment =>
.Select<PaymentEntity, Task<object>>(async payment =>
{
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
paymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(paymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
else
{
confirmationCount = onChainPaymentData.ConfirmationCount;
}
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
}
else
{
confirmationCount = paymentData.ConfirmationCount;
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
return new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
};
}
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = paymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
})
.ToArray();
await Task.WhenAll(payments);
await Task.WhenAll(onChainPayments);
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
model.StatusMessage = StatusMessage;
return View(model);
}
@ -220,6 +232,7 @@ namespace BTCPayServer.Controllers
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
@ -238,7 +251,8 @@ namespace BTCPayServer.Controllers
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
NetworkFee = paymentMethodDetails.GetTxFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
AllowCoinConversion = storeBlob.AllowCoinConversion,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
@ -251,12 +265,8 @@ namespace BTCPayServer.Controllers
.ToList()
};
var isMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration);
model.TimeLeft = expiration.PrettyPrint();
return model;
}
@ -275,17 +285,6 @@ namespace BTCPayServer.Controllers
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
}
private string PrettyPrint(TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
if (expiration.Days >= 1)
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
if (expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
return builder.ToString();
}
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
@ -358,7 +357,7 @@ namespace BTCPayServer.Controllers
[Route("invoices")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
@ -368,15 +367,15 @@ namespace BTCPayServer.Controllers
Count = count,
Skip = skip,
UserId = GetUserId(),
Status = filterString.Filters.TryGet("status"),
StoreId = filterString.Filters.TryGet("storeid")
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
Date = Prettify(invoice.InvoiceTime),
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
@ -389,30 +388,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
private string Prettify(DateTimeOffset invoiceTime)
{
var ago = DateTime.UtcNow - invoiceTime;
if (ago.TotalMinutes < 1)
{
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
}
if (ago.TotalHours < 1)
{
return $"{(int)ago.TotalMinutes} minute{Plural((int)ago.TotalMinutes)} ago";
}
if (ago.Days < 1)
{
return $"{(int)ago.TotalHours} hour{Plural((int)ago.TotalHours)} ago";
}
return $"{(int)ago.TotalDays} day{Plural((int)ago.TotalDays)} ago";
}
private string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
@ -443,14 +418,17 @@ namespace BTCPayServer.Controllers
StatusMessage = null;
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You need to be owner of this store to create an invoice";
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);
}
if(StatusMessage != null)
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
return View(model);
}
if (StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{

@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob)))
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers
{
var btc = _NetworkProvider.BTC;
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules());
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
@ -183,15 +183,16 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob)
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency);
var storeBlob = store.GetStoreBlob();
var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network);
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
@ -220,7 +221,7 @@ namespace BTCPayServer.Controllers
if (limitValue.Currency == entity.ProductInformation.Currency)
limitValueRate = paymentMethod.Rate;
else
limitValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(limitValue.Currency);
limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
@ -277,11 +278,6 @@ namespace BTCPayServer.Controllers
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
{
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));

@ -49,18 +49,20 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
if (rateProvider == null)
return NotFound();
RateRules rules = null;
if (storeId != null)
{
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return NotFound();
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
rules = store.GetStoreBlob().GetRateRules();
}
var rateProvider = _RateProviderFactory.GetRateProvider(network, rules);
if (rateProvider == null)
return NotFound();
var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r =>
new NBitpayClient.Rate()

@ -1,7 +1,9 @@
using BTCPayServer.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@ -11,6 +13,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mail;
using System.Threading.Tasks;
@ -21,11 +24,88 @@ namespace BTCPayServer.Controllers
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private CssThemeManager _CssThemeManager;
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
public ServerController(UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository,
CssThemeManager cssThemeManager)
{
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
_CssThemeManager = cssThemeManager;
}
[Route("server/rates")]
public async Task<IActionResult> Rates()
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
var vm = new RatesViewModel()
{
CacheMinutes = rates.CacheInMinutes,
PrivateKey = rates.PrivateKey,
PublicKey = rates.PublicKey
};
await FetchRateLimits(vm);
return View(vm);
}
private static async Task FetchRateLimits(RatesViewModel vm)
{
var coinAverage = GetCoinaverageService(vm, false);
if (coinAverage != null)
{
try
{
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
}
catch { }
}
}
[Route("server/rates")]
[HttpPost]
public async Task<IActionResult> Rates(RatesViewModel vm)
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
rates.PrivateKey = vm.PrivateKey;
rates.PublicKey = vm.PublicKey;
rates.CacheInMinutes = vm.CacheMinutes;
try
{
var service = GetCoinaverageService(vm, true);
if(service != null)
await service.TestAuthAsync();
}
catch
{
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
}
if (!ModelState.IsValid)
{
await FetchRateLimits(vm);
return View(vm);
}
await _SettingsRepository.UpdateSetting(rates);
StatusMessage = "Rate settings successfully updated";
return RedirectToAction(nameof(Rates));
}
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
{
var settings = new CoinAverageSettings()
{
KeyPair = (vm.PublicKey, vm.PrivateKey)
};
if (!withAuth || settings.GetCoinAverageSignature() != null)
{
return new CoinAverageRateProvider("BTC")
{ Authenticator = settings };
}
return null;
}
[Route("server/users")]
@ -52,6 +132,7 @@ namespace BTCPayServer.Controllers
var roles = await _UserManager.GetRolesAsync(user);
var userVM = new UserViewModel();
userVM.Id = user.Id;
userVM.Email = user.Email;
userVM.IsAdmin = IsAdmin(roles);
return View(userVM);
}
@ -72,7 +153,7 @@ namespace BTCPayServer.Controllers
var isAdmin = IsAdmin(roles);
bool updated = false;
if(isAdmin != viewModel.IsAdmin)
if (isAdmin != viewModel.IsAdmin)
{
if (viewModel.IsAdmin)
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
@ -80,7 +161,7 @@ namespace BTCPayServer.Controllers
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
updated = true;
}
if(updated)
if (updated)
{
viewModel.StatusMessage = "User successfully updated";
}
@ -138,7 +219,25 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies upadated successfully";
TempData["StatusMessage"] = "Policies updated successfully";
return View(settings);
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
return View(data);
}
[Route("server/theme")]
[HttpPost]
public async Task<IActionResult> Theme(ThemeSettings settings)
{
await _SettingsRepository.UpdateSetting(settings);
// TODO: remove controller/class-level property and have only reference to
// CssThemeManager here in this method
_CssThemeManager.Update(settings);
TempData["StatusMessage"] = "Theme settings updated successfully";
return View(settings);
}

@ -26,10 +26,16 @@ namespace BTCPayServer.Controllers
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
return View(vm);
}
@ -63,6 +69,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
vm.RootKeyPath = network.GetRootKeyPath();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
@ -204,7 +211,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination);
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -251,8 +258,6 @@ namespace BTCPayServer.Controllers
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account);
;
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
result = getxpubResult;
}
if (command == "getinfo")

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
using System.Threading;
namespace BTCPayServer.Controllers
{
@ -127,13 +128,20 @@ namespace BTCPayServer.Controllers
try
{
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
}
return View(vm);
}
}

@ -5,6 +5,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
@ -47,7 +48,8 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
IHostingEnvironment env,
CoinAverageSettings coinAverage)
{
_Dashboard = dashboard;
_Repo = repo;
@ -64,7 +66,9 @@ namespace BTCPayServer.Controllers
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_CoinAverage = coinAverage;
}
CoinAverageSettings _CoinAverage;
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
@ -197,6 +201,7 @@ namespace BTCPayServer.Controllers
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
return View(vm);
@ -237,12 +242,13 @@ namespace BTCPayServer.Controllers
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang);
if(!ModelState.IsValid)
if (!ModelState.IsValid)
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
@ -273,6 +279,7 @@ namespace BTCPayServer.Controllers
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;
@ -282,7 +289,7 @@ namespace BTCPayServer.Controllers
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
return View(vm);
}
@ -324,6 +331,7 @@ namespace BTCPayServer.Controllers
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
if (!ModelState.IsValid)
{
return View(model);
@ -356,6 +364,7 @@ namespace BTCPayServer.Controllers
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;
@ -369,14 +378,11 @@ namespace BTCPayServer.Controllers
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
{
using (HttpClient client = new HttpClient())
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
{
var rate = await client.GetAsync(model.RateSource);
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
}
@ -392,9 +398,14 @@ namespace BTCPayServer.Controllers
});
}
private (String DisplayName, String Name)[] GetSupportedExchanges()
{
return new[] { ("Coin Average", "coinaverage") }.Concat(_CoinAverage.AvailableExchanges).ToArray();
}
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
{
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
parser.HintScriptPubKey = hint;
return new DerivationStrategy(parser.Parse(derivationScheme), network);
}

@ -214,6 +214,7 @@ namespace BTCPayServer.Data
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
{
@ -223,6 +224,9 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
@ -270,30 +274,26 @@ namespace BTCPayServer.Data
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomCSS { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
{
if (!PreferredExchange.IsCoinAverage())
get
{
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
if (rateProvider is CachedRateProvider cachedRateProvider)
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
cachedRateProvider.Inner
});
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
}
else
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
rateProvider
});
}
return _LightningDescriptionTemplate ?? "Paid to {StoreName} (Order ID: {OrderId})";
}
if (RateRules == null || RateRules.Count == 0)
return rateProvider;
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
set
{
_LightningDescriptionTemplate = value;
}
}
public RateRules GetRateRules()
{
return new RateRules(RateRules)
{
PreferredExchange = PreferredExchange
};
}
}
}

@ -13,13 +13,11 @@ namespace BTCPayServer
public class DerivationSchemeParser
{
public Network Network { get; set; }
public ChainType ChainType { get; set; }
public Script HintScriptPubKey { get; set; }
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
public DerivationSchemeParser(Network expectedNetwork)
{
Network = expectedNetwork;
ChainType = chainType;
}
public DerivationStrategyBase Parse(string str)
@ -43,6 +41,9 @@ namespace BTCPayServer
}
}
if(!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
try
{
var result = new DerivationStrategyFactory(Network).Parse(str);
@ -75,7 +76,7 @@ namespace BTCPayServer
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];

@ -28,11 +28,44 @@ using BTCPayServer.Payments;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Models;
using System.Security.Claims;
using System.Globalization;
namespace BTCPayServer
{
public static class Extensions
{
public static string Prettify(this TimeSpan timeSpan)
{
if (timeSpan.TotalMinutes < 1)
{
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
}
if (timeSpan.TotalHours < 1)
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
public static string PrettyPrint(this TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
if (expiration.Days >= 1)
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
if (expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
return builder.ToString();
}
public static decimal RoundUp(decimal value, int precision)
{
for (int i = 0; i < precision; i++)

@ -0,0 +1,62 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using NBXplorer.Models;
using System.Collections.Concurrent;
using BTCPayServer.Events;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class CssThemeManager
{
public CssThemeManager(SettingsRepository settingsRepository)
{
Update(settingsRepository);
}
private async void Update(SettingsRepository settingsRepository)
{
var data = (await settingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
Update(data);
}
public void Update(ThemeSettings data)
{
UpdateBootstrap(data.BootstrapCssUri);
UpdateCreativeStart(data.CreativeStartCssUri);
}
private string _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
public string BootstrapUri
{
get { return _bootstrapUri; }
}
public void UpdateBootstrap(string newUri)
{
if (String.IsNullOrWhiteSpace(newUri))
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v="+ DateTime.Now.Ticks;
else
_bootstrapUri = newUri;
}
private string _creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
public string CreativeStartUri
{
get { return _creativeStartUri; }
}
public void UpdateCreativeStart(string newUri)
{
if (String.IsNullOrWhiteSpace(newUri))
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
else
_creativeStartUri = newUri;
}
}
}

@ -209,16 +209,16 @@ namespace BTCPayServer.HostedServices
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
try
{
var now = DateTimeOffset.UtcNow;
if (invoice.ExpirationTime > now)
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
now = DateTimeOffset.UtcNow;
if (invoice.MonitoringExpiration > now)
delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
}

@ -184,8 +184,8 @@ namespace BTCPayServer.HostedServices
if(status != null && error == null)
{
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
if(status.NetworkType != _Network.NBitcoinNetwork.NetworkType)
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.NetworkType}, expected: {_Network.NBitcoinNetwork.NetworkType})";
}
if (error != null)

@ -0,0 +1,106 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging;
using System.Runtime.CompilerServices;
using System.IO;
using System.Text;
namespace BTCPayServer.HostedServices
{
public class RatesHostedService : IHostedService
{
private SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
private CoinAverageSettings _coinAverageSettings;
public RatesHostedService(SettingsRepository repo,
CoinAverageSettings coinAverageSettings,
IRateProviderFactory rateProviderFactory)
{
this._SettingsRepository = repo;
_RateProviderFactory = rateProviderFactory;
_coinAverageSettings = coinAverageSettings;
}
CancellationTokenSource _Cts = new CancellationTokenSource();
List<Task> _Tasks = new List<Task>();
public Task StartAsync(CancellationToken cancellationToken)
{
_Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token));
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token));
return Task.CompletedTask;
}
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
{
await new SynchronizationContextRemover();
while (!cancellation.IsCancellationRequested)
{
try
{
await act();
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, caller + " failed");
try
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
}
}
}
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
{
return Timer(async () =>
{
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
_coinAverageSettings.AvailableExchanges = tickers
.Exchanges
.Select(c => (c.DisplayName, c.Name))
.ToArray();
await Task.Delay(TimeSpan.FromHours(5), cancellation);
}, cancellation);
}
Task RefreshCoinAverageSettings(CancellationToken cancellation)
{
return Timer(async () =>
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
{
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
}
else
{
_coinAverageSettings.KeyPair = null;
}
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(cancellation);
}, cancellation);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return Task.WhenAll(_Tasks.ToArray());
}
}
}

@ -109,6 +109,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -136,6 +138,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<CssThemeManager>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
@ -154,16 +157,17 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
{
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
if (o.GetRequiredService<BTCPayServerOptions>().NetworkType == NetworkType.Mainnet)
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<IRateProviderFactory, CachedDefaultRateProviderFactory>();
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();

@ -76,8 +76,6 @@ namespace BTCPayServer.Hosting
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Big hack, tests fails because Hangfire fail at initializing at the second test run
AddHangfireFix(services);
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -93,6 +91,24 @@ namespace BTCPayServer.Hosting
options.Password.RequireUppercase = false;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
@ -103,51 +119,29 @@ namespace BTCPayServer.Hosting
//});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
private void AddHangfireFix(IServiceCollection services)
{
Action<IGlobalConfiguration> configuration = o =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
};
ServiceCollectionDescriptorExtensions.TryAddSingleton<Action<IGlobalConfiguration>>(services, (IServiceProvider serviceProvider) => new Action<IGlobalConfiguration>((config) =>
{
ILoggerFactory service = ServiceProviderServiceExtensions.GetService<ILoggerFactory>(serviceProvider);
if (service != null)
{
Hangfire.GlobalConfigurationExtensions.UseLogProvider<AspNetCoreLogProvider>(config, new AspNetCoreLogProvider(service));
}
IServiceScopeFactory service2 = ServiceProviderServiceExtensions.GetService<IServiceScopeFactory>(serviceProvider);
if (service2 != null)
{
Hangfire.GlobalConfigurationExtensions.UseActivator<AspNetCoreJobActivator>(config, new AspNetCoreJobActivator(service2));
}
configuration(config);
}));
services.AddHangfire(configuration);
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IServiceProvider prov,
BTCPayServerOptions options,
ILoggerFactory loggerFactory)
{
Logs.Configure(loggerFactory);
Logs.Configuration.LogInformation($"Root Path: {options.RootPath}");
if (options.RootPath.Equals("/", StringComparison.OrdinalIgnoreCase))
{
ConfigureCore(app, env, prov, loggerFactory, options);
}
else
{
app.Map(options.RootPath, appChild =>
{
ConfigureCore(appChild, env, prov, loggerFactory, options);
});
}
}
private static void ConfigureCore(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
{
if (env.IsDevelopment())
{
@ -155,9 +149,6 @@ namespace BTCPayServer.Hosting
app.UseBrowserLink();
}
Logs.Configure(loggerFactory);
//App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
@ -165,7 +156,11 @@ namespace BTCPayServer.Hosting
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseWebSockets();
app.UseMvc(routes =>
{

@ -17,5 +17,8 @@ namespace BTCPayServer.Models.AppViewModels
[Required]
[MaxLength(5000)]
public string Template { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
}
}

@ -18,6 +18,8 @@ namespace BTCPayServer.Models.AppViewModels
public ItemPrice Price { get; set; }
public string Title { get; set; }
}
public bool ShowCustomAmount { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }
}

@ -27,7 +27,7 @@ namespace BTCPayServer.Models.InvoicingModels
}
public class Payment
{
public string PaymentMethod { get; set; }
public string Crypto { get; set; }
public string Confirmations
{
get; set;
@ -72,7 +72,13 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
} = new List<CryptoPayment>();
public List<Payment> Payments { get; set; } = new List<Payment>();
public List<Payment> OnChainPayments { get; set; } = new List<Payment>();
public List<OffChainPayment> OffChainPayments { get; set; } = new List<OffChainPayment>();
public class OffChainPayment
{
public string Crypto { get; set; }
public string BOLT11 { get; set; }
}
public string Status
{

@ -24,6 +24,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string BtcAddress { get; set; }
public string BtcDue { get; set; }
public string CustomerEmail { get; set; }
public bool RequiresRefundEmail { get; set; }
public int ExpirationSeconds { get; set; }
public string Status { get; set; }
public string MerchantRefLink { get; set; }
@ -43,7 +44,8 @@ namespace BTCPayServer.Models.InvoicingModels
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public string NetworkFeeDescription { get; internal set; }
public decimal NetworkFee { get; set; }
public bool IsMultiCurrency { get; set; }
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Models.ServerViewModels
{
public class RatesViewModel
{
[Display(Name = "Bitcoin average api keys")]
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
[Display(Name = "Cache the rates for ... minutes")]
[Range(0, 60)]
public int CacheMinutes { get; set; }
public GetRateLimitsResponse RateLimits { get; internal set; }
}
}

@ -31,6 +31,12 @@ namespace BTCPayServer.Models.StoreViewModels
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail
{
get; set;
}
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
[MaxLength(20)]
public string OnChainMinValue { get; set; }

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
namespace BTCPayServer.Models.StoreViewModels
{
@ -29,5 +30,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }
public KeyPath RootKeyPath { get; set; }
}
}

@ -23,5 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
}
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public bool SkipPortTest { get; set; }
}
}

@ -12,6 +12,11 @@ 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; }
@ -44,6 +49,17 @@ 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; }
@ -64,7 +80,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 31)]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration
{
get;
@ -72,7 +88,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
[Range(10, 60 * 24 * 31)]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration
{
get;
@ -91,6 +107,9 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }
@ -100,6 +119,5 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
} = new List<LightningNode>();
}
}

@ -1111,4 +1111,17 @@ namespace BTCPayServer
#endregion
}
}
public static class MultiValueDictionaryExtensions
{
public static MultiValueDictionary<TKey, TValue> ToMultiValueDictionary<TInput, TKey, TValue>(this IEnumerable<TInput> collection, Func<TInput, TKey> keySelector, Func<TInput, TValue> valueSelector)
{
var dictionary = new MultiValueDictionary<TKey, TValue>();
foreach(var item in collection)
{
dictionary.Add(keySelector(item), valueSelector(item));
}
return dictionary;
}
}
}

@ -27,7 +27,7 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments
@ -16,25 +17,26 @@ namespace BTCPayServer.Payments
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="paymentMethod"></param>
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
}
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
}
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, network);
return CreatePaymentMethodDetails(method, paymentMethod, store, network);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}

@ -174,10 +174,10 @@ namespace BTCPayServer.Payments.Lightning.CLightning
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, description ?? "", Math.Max(0, (int)expiry.TotalSeconds) }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";

@ -48,8 +48,10 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
var message = CreateMessage(HttpMethod.Post, "invoice");
Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
if(request.Description != null)
parameters.Add("description", request.Description);
message.Content = new FormUrlEncodedContent(parameters);
var result = await _Client.SendAsync(message, cancellation);
result.EnsureSuccessStatusCode();
@ -152,9 +154,9 @@ namespace BTCPayServer.Payments.Lightning.Charge
};
}
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry });
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}

@ -7,7 +7,8 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceRequest
{
public LightMoney Amont { get; set; }
public LightMoney Amount { get; set; }
public TimeSpan Expiry { get; set; }
public string Description { get; set; }
}
}

@ -29,7 +29,7 @@ namespace BTCPayServer.Payments.Lightning
public interface ILightningInvoiceClient
{
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
}

@ -5,6 +5,7 @@ using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
@ -23,8 +24,9 @@ namespace BTCPayServer.Payments.Lightning
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
var storeBlob = store.GetStoreBlob();
var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
@ -36,9 +38,13 @@ namespace BTCPayServer.Payments.Lightning
LightningInvoice lightningInvoice = null;
try
{
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
}
catch(Exception ex)
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
}
@ -51,11 +57,6 @@ namespace BTCPayServer.Payments.Lightning
};
}
/// <summary>
/// Used for testing
/// </summary>
public bool SkipP2PTest { get; set; }
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
@ -88,38 +89,34 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
{
try
{
if (!SkipP2PTest)
IPAddress address = null;
try
{
await TestConnection(info.Address, info.P2PPort, cts.Token);
address = IPAddress.Parse(nodeInfo.Host);
}
catch
{
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)), cancellation);
}
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
private async Task TestConnection(string addressStr, int port, CancellationToken cancellation)
{
IPAddress address = null;
try
{
address = IPAddress.Parse(addressStr);
}
catch
{
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {addressStr}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})");
}
}

@ -52,9 +52,20 @@ namespace BTCPayServer.Payments.Lightning
_ListenPoller = new Timer(async s =>
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
try
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
}
catch (AggregateException ex)
{
Logs.PayServer.LogError(ex.InnerException ?? ex.InnerExceptions.FirstOrDefault(), $"Lightning: Uncaught error");
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Lightning: Uncaught error");
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
return Task.CompletedTask;
@ -90,7 +101,16 @@ namespace BTCPayServer.Payments.Lightning
if (poll)
{
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
LightningInvoice chargeInvoice = null;
try
{
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
}
catch(Exception ex)
{
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
continue;
}
if (chargeInvoice == null)
continue;
if (chargeInvoice.Status == "paid")

@ -21,7 +21,7 @@ namespace BTCPayServer
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
.Where(kv => kv.Length == 2)
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
.ToDictionary(o => o.Key, o => o.Value);
.ToMultiValueDictionary(o => o.Key, o => o.Value);
foreach(var filter in splitted)
{
@ -38,8 +38,8 @@ namespace BTCPayServer
get;
private set;
}
public Dictionary<string, string> Filters { get; private set; }
public MultiValueDictionary<string, string> Filters { get; private set; }
public override string ToString()
{

@ -6,6 +6,7 @@ using System.Reflection;
using System.Threading.Tasks;
using System.Text;
using NBXplorer;
using NBitcoin;
namespace BTCPayServer.Services
{
@ -20,13 +21,13 @@ namespace BTCPayServer.Services
Build = "Release";
#endif
Environment = env;
ChainType = provider.NBXplorerNetworkProvider.ChainType;
NetworkType = provider.NetworkType;
}
public IHostingEnvironment Environment
{
get; set;
}
public ChainType ChainType { get; set; }
public NetworkType NetworkType { get; set; }
public string Version
{
get; set;
@ -40,7 +41,7 @@ namespace BTCPayServer.Services
{
get
{
return ChainType == ChainType.Regtest && Environment.IsDevelopment();
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
}
}
public override string ToString()

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
@ -82,12 +83,13 @@ namespace BTCPayServer.Services
if (network == null)
throw new ArgumentNullException(nameof(network));
var path = new KeyPath("49'").Derive(network.CoinType).Derive(account, true);
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var path = network.GetRootKeyPath().Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
P2SH = segwit,
Legacy = !segwit
});
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
@ -97,10 +99,14 @@ namespace BTCPayServer.Services
try
{
var pubKey = await ledger.GetWalletPubKeyAsync(account);
if (pubKey.Address.Network != network.NBitcoinNetwork)
try
{
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
pubKey.GetAddress(network.NBitcoinNetwork);
}
catch
{
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
}
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
@ -125,9 +131,13 @@ namespace BTCPayServer.Services
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
List<KeyPath> derivations = new List<KeyPath>();
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
derivations.Add(new KeyPath("49'"));
derivations.Add(new KeyPath("44'"));
KeyPath foundKeyPath = null;
foreach (var account in
new[] { new KeyPath("49'"), new KeyPath("44'") }
derivations
.Select(purpose => purpose.Derive(network.CoinType))
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
{
@ -189,6 +199,7 @@ namespace BTCPayServer.Services
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send)
@ -235,6 +246,5 @@ namespace BTCPayServer.Services
public string ExtPubKey { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int CoinType { get; internal set; }
}
}

@ -399,9 +399,10 @@ namespace BTCPayServer.Services.Invoices
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (!string.IsNullOrEmpty(queryObject.StoreId))
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
@ -429,8 +430,11 @@ namespace BTCPayServer.Services.Invoices
if (queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId);
if (queryObject.Status != null)
query = query.Where(i => i.Status == queryObject.Status);
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
query = query.OrderByDescending(q => q.Created);
@ -568,7 +572,7 @@ namespace BTCPayServer.Services.Invoices
public class InvoiceQuery
{
public string StoreId
public string[] StoreId
{
get; set;
}
@ -610,7 +614,7 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
public string Status
public string[] Status
{
get; set;
}

@ -26,6 +26,7 @@ namespace BTCPayServer.Services
new Language("ja-JP", "日本語"),
new Language("fr-FR", "Français"),
new Language("es-ES", "Spanish"),
new Language("pt-PT", "Portuguese"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("cs-CZ", "Česky"),

@ -0,0 +1,95 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class BTCPayRateProviderFactory : IRateProviderFactory
{
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, IServiceProvider serviceProvider)
{
if (cacheOptions == null)
throw new ArgumentNullException(nameof(cacheOptions));
_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;
}
IServiceProvider serviceProvider;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
_Cache = new MemoryCache(_CacheOptions);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, 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);
}
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
{
List<IRateProvider> providers = new List<IRateProvider>();
if(exchange == "quadrigacx")
{
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
}
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)
{
throw new RateUnavailableException(network.CryptoCode);
}
return network.DefaultRateProvider.CreateRateProvider(serviceProvider);
}
}
}

@ -4,9 +4,17 @@ using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
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
{
Bitpay _Bitpay;

@ -1,43 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedDefaultRateProviderFactory : IRateProviderFactory
{
IMemoryCache _Cache;
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
ConcurrentDictionary<string, IRateProvider> _LongCacheProviders = new ConcurrentDictionary<string, IRateProvider>();
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
public CachedDefaultRateProviderFactory(IMemoryCache cache)
{
if (cache == null)
throw new ArgumentNullException(nameof(cache));
_Cache = cache;
// Using same providers because they are both at 15 min actually...
_Providers = _LongCacheProviders;
}
public IRateProvider RateProvider { get; set; }
// We use 15 min because of limits with free version of bitcoinaverage
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public TimeSpan LongCacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{
return (longCache ? _LongCacheProviders : _Providers).GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = longCache ? LongCacheSpan : CacheSpan, AdditionalScope = longCache ? "LONG" : "SHORT" });
}
}
}

@ -1,11 +1,15 @@
using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
namespace BTCPayServer.Services.Rates
{
@ -16,6 +20,57 @@ 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
{
public string Name { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
public string[] Symbols { get; set; }
}
public bool Success { get; set; }
public Exchange[] Exchanges { get; set; }
}
public class RatesSetting
{
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int CacheInMinutes { get; set; } = 15;
}
public interface ICoinAverageAuthenticator
{
Task AddHeader(HttpRequestMessage message);
}
public class CoinAverageRateProvider : IRateProvider
{
static HttpClient _Client = new HttpClient();
@ -48,17 +103,20 @@ namespace BTCPayServer.Services.Rates
throw new RateUnavailableException(currency);
}
public ICoinAverageAuthenticator Authenticator { get; set; }
private async Task<Dictionary<string, decimal>> GetRatesCore()
{
HttpResponseMessage resp = null;
if (Exchange == null)
string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var auth = Authenticator;
if (auth != null)
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
}
else
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}");
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
using (resp)
{
@ -99,5 +157,82 @@ namespace BTCPayServer.Services.Rates
Value = o.Value
}).ToList();
}
public async Task TestAuthAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
}
public async Task<GetRateLimitsResponse> GetRateLimitsAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/info/ratelimits");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetRateLimitsResponse();
response.CounterReset = TimeSpan.FromSeconds(jobj["counter_reset"].Value<int>());
var totalPeriod = jobj["total_period"].Value<string>();
if (totalPeriod == "24h")
{
response.TotalPeriod = TimeSpan.FromHours(24);
}
else if (totalPeriod == "30d")
{
response.TotalPeriod = TimeSpan.FromDays(30);
}
else
{
response.TotalPeriod = TimeSpan.FromSeconds(jobj["total_period"].Value<int>());
}
response.RequestsLeft = jobj["requests_left"].Value<int>();
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
return response;
}
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();
response.Success = jobj["success"].Value<bool>();
var exchanges = (JObject)jobj["exchanges"];
response.Exchanges = exchanges
.Properties()
.Select(p =>
{
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
exchange.Name = p.Name;
return exchange;
})
.ToArray();
return response;
}
}
public class GetRateLimitsResponse
{
public TimeSpan CounterReset { get; set; }
public int RequestsLeft { get; set; }
public int RequestsPerPeriod { get; set; }
public TimeSpan TotalPeriod { get; set; }
}
}

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
{
CoinAverageSettings _Settings;
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
{
_Settings = settings;
}
public Task AddHeader(HttpRequestMessage message)
{
return _Settings.AddHeader(message);
}
}
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 CoinAverageSettings()
{
//GENERATED BY:
//StringBuilder b = new StringBuilder();
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
//{
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new[] {
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "EtherDelta", Name: "etherdelta"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "GDAX", Name: "gdax"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "Gemini", Name: "gemini"),
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "itBit", Name: "itbit"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "Foxbit", Name: "foxbit"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Liqui", Name: "liqui"),
(DisplayName: "BitKonan", Name: "bitkonan"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "Coinsecure", Name: "coinsecure"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "CampBX", Name: "campbx"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Bitso", Name: "bitso"),
}.ToArray();
}
public Task AddHeader(HttpRequestMessage message)
{
var signature = GetCoinAverageSignature();
if (signature != null)
{
message.Headers.Add("X-signature", signature);
}
return Task.CompletedTask;
}
public string GetCoinAverageSignature()
{
var keyPair = KeyPair;
if (!keyPair.HasValue)
return null;
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
return null;
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
var payload = timestamp + "." + keyPair.Value.PublicKey;
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
return payload + "." + digestValueHex;
}
}
}

@ -5,8 +5,24 @@ using System.Threading.Tasks;
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 FallbackRateProvider(IRateProvider[] providers)
{

@ -1,12 +1,40 @@
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, bool longCache);
IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules);
TimeSpan CacheSpan { get; set; }
void InvalidateCache();
}
}

@ -14,14 +14,21 @@ namespace BTCPayServer.Services.Rates
}
public TimeSpan CacheSpan { get; set; }
public void AddMock(MockRateProvider mock)
{
_Mocks.Add(mock);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules)
{
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
}
public void InvalidateCache()
{
}
}
public class MockRateProvider : IRateProvider
{

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class QuadrigacxRateProvider : IRateProvider
{
public QuadrigacxRateProvider(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
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)
{
v = 0.0m;
JToken token = p.Property("bid")?.Value;
if (token == null)
return false;
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 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>();
foreach (var prop in rates.Properties())
{
var rate = new Rate();
var splitted = prop.Name.Split('_');
var crypto = splitted[0].ToUpperInvariant();
if (crypto != CryptoCode)
continue;
rate.Currency = splitted[1].ToUpperInvariant();
TryToDecimal((JObject)prop.Value, out var v);
rate.Value = v;
result.Add(rate);
}
return result;
}
}
}

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

@ -10,9 +10,9 @@ namespace BTCPayServer.Services.Rates
{
private BTCPayNetwork network;
private IRateProvider rateProvider;
private List<RateRule> rateRules;
private RateRules rateRules;
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List<RateRule> rateRules)
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules)
{
if (network == null)
throw new ArgumentNullException(nameof(network));

@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Newtonsoft.Json;
using System.Threading;
namespace BTCPayServer.Services
{
@ -51,6 +52,22 @@ namespace BTCPayServer.Services
await ctx.SaveChangesAsync();
}
}
IReadOnlyCollection<TaskCompletionSource<bool>> value;
lock (_Subscriptions)
{
if(_Subscriptions.TryGetValue(typeof(T), out value))
{
_Subscriptions.Remove(typeof(T));
}
}
if(value != null)
{
foreach(var v in value)
{
v.TrySetResult(true);
}
}
}
private T Deserialize<T>(string value)
@ -62,5 +79,35 @@ namespace BTCPayServer.Services
{
return JsonConvert.SerializeObject(obj);
}
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
try
{
tcs.TrySetCanceled();
}
catch { }
}))
{
lock (_Subscriptions)
{
_Subscriptions.Add(typeof(T), tcs);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
tcs.Task.ContinueWith(_ =>
{
lock (_Subscriptions)
{
_Subscriptions.Remove(typeof(T), tcs);
}
}, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
await tcs.Task;
}
}
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
public class ThemeSettings
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public string BootstrapCssUri { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public string CreativeStartCssUri { get; set; }
}
}

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public struct SynchronizationContextRemover : INotifyCompletion
{
public bool IsCompleted => SynchronizationContext.Current == null;
public void OnCompleted(Action continuation)
{
var prev = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(null);
continuation();
}
finally
{
SynchronizationContext.SetSynchronizationContext(prev);
}
}
public SynchronizationContextRemover GetAwaiter()
{
return this;
}
public void GetResult()
{
}
}
}

@ -22,7 +22,7 @@
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>

@ -3,23 +3,37 @@
ViewData["Title"] = "Forgot your password?";
}
<h2>@ViewData["Title"]</h2>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<h4>Start password reset</h4>
<hr />
<p>
We all forget passwords every now and then. Just provide email address tied to your account and we'll start the process of helping you recover your account.
</p>
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -1,8 +1,22 @@
@{
ViewData["Title"] = "Forgot password confirmation";
ViewData["Title"] = "Email sent!";
}
<h2>@ViewData["Title"]</h2>
<h2></h2>
<p>
Please check your email to reset your password.
</p>
<section>
<div class="container">
<div class="row">
<div class="col-md-4">
<h4>@ViewData["Title"]</h4>
<hr />
<p>
Please check your email to reset your password.
</p>
</div>
</div>
</div>
</section>

@ -41,7 +41,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
<div class="form-group">
<p>
@ -76,7 +76,7 @@
<p>
@foreach(var provider in loginProviders)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
}
</p>
</div>

@ -31,7 +31,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
</form>
</div>

@ -18,7 +18,7 @@
<input asp-for="RecoveryCode" class="form-control" autocomplete="off" />
<span asp-validation-for="RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</form>
</div>
</div>

@ -35,7 +35,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>

@ -26,7 +26,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Reset</button>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
</div>

@ -28,7 +28,7 @@
<select asp-for="SelectedStore" asp-items="Model.Stores" class="form-control"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>

@ -21,9 +21,9 @@
</div>
<div class="row">
<a asp-action="CreateApp" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span> Create a new app</a>
<table class="table">
<thead class="thead-inverse">
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Store</th>
<th>Name</th>
@ -32,11 +32,11 @@
</tr>
</thead>
<tbody>
@foreach(var app in Model.Apps)
@foreach (var app in Model.Apps)
{
<tr>
<td>
@if(app.IsOwner)
@if (app.IsOwner)
{
<span><a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
}
@ -48,7 +48,7 @@
<td>@app.AppName</td>
<td>@app.AppType</td>
<td style="text-align:right">
@if(app.IsOwner)
@if (app.IsOwner)
{
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a><span> - </span>
}

@ -29,13 +29,17 @@
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-success" />
<input type="submit" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>

@ -11,11 +11,11 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="~/vendor/bootstrap/css/bootstrap.css" />
<link rel="stylesheet" href="~/vendor/bootstrap4/css/bootstrap.css" />
</head>
<body class="h-100">
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto">
<div class="justify-content-center align-self-center text-center mx-auto" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
<form method="post">
<div class="row">
@ -23,26 +23,28 @@
{
<div class="col-sm-4 mb-3">
<h3>@item.Title</h3>
<button type="submit" name="choiceKey" class="btn btn-secondary" value="@item.Id">Buy for @item.Price.Formatted</button>
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
</div>
}
</div>
</form>
@*<div class="row mt-4">
@if(Model.ShowCustomAmount)
{
<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<h3>Something else</h3>
<form data-buy>
<form method="post" data-buy>
<div class="input-group">
<input class="form-control" type="number" step="0.00001" name="amount" placeholder="undefined (optional)"><div class="input-group-append">
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
<button class="btn btn-primary" type="submit">Pay</button>
</div>
</div>
</form>
</div>
</div>*@
</div>
}
</div>
</div>
<script src="~/vendor/bootstrap/js/bootstrap.js"></script>
<script src="~/vendor/jquery/jquery.js"></script>
<script src="~/vendor/bootstrap4/js/bootstrap.js"></script>
</body>
</html>

@ -6,8 +6,9 @@
<header class="masthead">
<div class="header-content">
<div class="header-content-inner">
<h1 id="homeHeading" style="padding-bottom:30px;">Welcome to BTCPay Server</h1>
<div class="header-content-inner text-white">
<h1>Welcome to BTCPay Server</h1>
<hr />
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business. The API is compatible with Bitpay service to allow seamless migration.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://github.com/btcpayserver/btcpayserver-doc">Getting started</a>
</div>
@ -28,21 +29,21 @@
<div class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/lock-logo.png" />
<h3>Secure</h3>
<h3 class="text-dark">Secure</h3>
<p class="text-muted">The payment server does not need to know your private keys, so your money can't be stolen.</p>
</div>
</div>
<div class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/qr-logo.png" />
<h3>Easy</h3>
<h3 class="text-dark">Easy</h3>
<p class="text-muted">A user-friendly Bitcoin checkout page for your customers.</p>
</div>
</div>
<div class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/money-logo.png" />
<h3>Visibility</h3>
<h3 class="text-dark">Visibility</h3>
<p class="text-muted">Manage, generate reports, and search for your invoices easily.</p>
</div>
</div>
@ -52,7 +53,7 @@
<div class="call-to-action bg-dark">
<div class="call-to-action bg-dark text-white">
<div class="container text-center">
<h2>Video tutorials</h2>
<div class="row">
@ -102,12 +103,12 @@
</div>
</section>
<div class="call-to-action bg-dark">
<div class="call-to-action bg-dark text-white">
<div class="container text-center">
<h2>Donate</h2>
<p>Donation to this address will be reinvested into the development of this tool</p>
<img src="~/img/donation.jpg">
<p style="font-size:10px">3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
<p><img src="~/img/donation.jpg"></p>
<p>3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
</div>
</div>
@ -117,7 +118,10 @@
<div class="col-lg-8 mx-auto text-center">
<h2 class="section-heading">Let's Get In Touch!</h2>
<hr class="primary">
<p>An open source project is nothing without its community, come and get in touch with us!</p>
<p>
An open source project is nothing without its community<br />
Come and get in touch with us!
</p>
</div>
</div>
<div class="row">

@ -87,7 +87,14 @@
<div class="line-items__item__label">
<span>{{$t("Network Cost")}}</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
<div class="line-items__item__value">
<span v-if="srvModel.IsMultiCurrency">
{{ srvModel.networkFee }} {{ srvModel.cryptoCode }}
</span>
<span v-else>
{{$t("txCount", {count: srvModel.txCount})}} x {{ srvModel.networkFee }} {{ srvModel.cryptoCode }}
</span>
</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
@ -156,7 +163,6 @@
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
@ -250,11 +256,9 @@
</div>
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<button class="action-button">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</button>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="srvModel.merchantRefLink">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</a>
</div>
</div>
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
@ -296,10 +300,9 @@
{{srvModel.orderId}}
</div>
</div>
<a href="/invoices" class="action-button" style="margin-top: 20px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="srvModel.merchantRefLink"
style="margin-top: 20px;">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</a>
</div>
</div>

@ -110,6 +110,7 @@
'es-ES': { translation: locales_es },
'ja-JP': { translation: locales_ja },
'fr-FR': { translation: locales_fr },
'pt': { translation: locales_pt },
'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl },
'cs-CZ': { translation: locales_cs },

@ -55,7 +55,7 @@
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListInvoices">Back to List</a>

@ -4,14 +4,21 @@
}
<style type="text/css">
.overflowbox {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.linethrough {
text-decoration: line-through;
}
.money {
text-align: right;
.smMaxWidth {
max-width: 200px;
}
@@media (min-width: 768px) {
.smMaxWidth {
max-width: 400px;
}
}
.firstCol {
width: 140px;
}
</style>
@ -28,14 +35,13 @@
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Invoice details</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<table class="table">
<table class="table table-sm table-responsive-md">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
@ -93,10 +99,9 @@
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table">
<table class="table table-sm table-responsive-md">
<tr>
<th>Name
<th>
<th>Name</th>
<td>@Model.BuyerInformation.BuyerName</td>
</tr>
<tr>
@ -134,7 +139,7 @@
</table>
<h3>Product information</h3>
<table class="table">
<table class="table table-sm table-responsive-md">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
@ -153,77 +158,109 @@
<div class="row">
<div class="col-md-12">
<h3>Paid summary</h3>
<table class="table">
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
<th>Payment method</th>
<th>Address</th>
<th class="money">Rate</th>
<th class="money">Paid</th>
<th class="money">Due</th>
<th class="text-right">Rate</th>
<th class="text-right">Paid</th>
<th class="text-right">Due</th>
</tr>
</thead>
<tbody>
@foreach(var payment in Model.CryptoPayments)
{
<tr>
<td>@payment.PaymentMethod</td>
<td>@payment.Address</td>
<td class="money">@payment.Rate</td>
<td class="money">@payment.Paid</td>
<td class="money">@payment.Due</td>
</tr>
}
@foreach (var payment in Model.CryptoPayments)
{
<tr>
<td>@payment.PaymentMethod</td>
<td>@payment.Address</td>
<td class="text-right">@payment.Rate</td>
<td class="text-right">@payment.Paid</td>
<td class="text-right">@payment.Due</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Payments</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
<th>Deposit address</th>
<th>Transaction Id</th>
<th style="text-align:right;">Confirmations</th>
</tr>
</thead>
<tbody>
@foreach(var payment in Model.Payments)
{
var replaced = payment.Replaced ? "text-decoration: line-through;" : "";
<tr>
<td style="@replaced">@payment.PaymentMethod</td>
<td style="@replaced">@payment.DepositAddress</td>
<td style="@replaced"><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
<td style="text-align:right;">@payment.Confirmations</td>
</tr>
}
</tbody>
</table>
@if (Model.OnChainPayments.Count > 0)
{
<div class="row">
<div class="col-md-12">
<h3>On-Chain payments</h3>
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Deposit address</th>
<th>Transaction Id</th>
<th class="text-right">Confirmations</th>
</tr>
</thead>
<tbody>
@foreach (var payment in Model.OnChainPayments)
{
var replaced = payment.Replaced ? "class='linethrough'" : "";
<tr @replaced>
<td>@payment.Crypto</td>
<td>@payment.DepositAddress</td>
<td class="smMaxWidth text-truncate">
<a href="@payment.TransactionLink" target="_blank">
@payment.TransactionId
</a>
</td>
<td class="text-right">@payment.Confirmations</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
@if (Model.OffChainPayments.Count > 0)
{
<div class="row">
<div class="col-md-12">
<h3>Off-Chain payments</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th class="firstCol">Crypto</th>
<th>BOLT11</th>
</tr>
</thead>
<tbody>
@foreach (var payment in Model.OffChainPayments)
{
<tr>
<td>@payment.Crypto</td>
<td class="smMaxWidth text-truncate">@payment.BOLT11</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<div class="row">
<div class="col-md-12">
<h3>Addresses</h3>
<table class="table">
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
<th class="firstCol">Payment method</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var address in Model.Addresses)
@foreach (var address in Model.Addresses)
{
var current = address.Current ? "font-weight: bold;" : "";
<tr>
<td style="width:100px;@current">@address.PaymentMethod</td>
<td style="max-width:100px;overflow:hidden;@current">@address.Destination</td>
</tr>
}
var current = address.Current ? "font-weight-bold" : "";
<tr>
<td>@address.PaymentMethod</td>
<td class="smMaxWidth text-truncate @current">@address.Destination</td>
</tr>
}
</tbody>
</table>
</div>
@ -232,7 +269,7 @@
<div class="row">
<div class="col-md-12">
<h3>Events</h3>
<table class="table">
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th>Date</th>
@ -240,7 +277,7 @@
</tr>
</thead>
<tbody>
@foreach(var evt in Model.Events)
@foreach (var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp</td>

@ -18,30 +18,39 @@
<hr class="primary">
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
<div id="help" class="collapse text-left">
<p>You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters</p>
<p>
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters
</p>
<ul>
<li><b>storeid:id</b> for filtering a specific store</li>
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
</ul>
<p>
If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`.
</p>
</div>
<div class="form-group">
<form asp-action="SearchInvoice" method="post">
<input asp-for="SearchTerm" class="form-control" />
<div class="input-group">
<input asp-for="SearchTerm" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary" title="Search invoice">
<span class="fa fa-search"></span> Search
</button>
</span>
</div>
<input type="hidden" asp-for="Count" />
<span asp-validation-for="SearchTerm" class="text-danger"></span>
<button type="submit" class="btn btn-default" title="Search invoice">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</form>
</div>
</div>
</div>
<div class="row">
<a asp-action="CreateInvoice" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span> Create a new invoice</a>
<table class="table">
<thead class="thead-inverse">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Date</th>
<th>OrderId</th>
@ -82,13 +91,13 @@
}
else
{
<td >@invoice.Status</td>
<td>@invoice.Status</td>
}
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if(invoice.Status == "new")
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
</td>
</tr>
@ -134,7 +143,7 @@
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Yes, make invoice Invalid</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
</div>
</div>

@ -1,7 +1,6 @@
@model ChangePasswordViewModel
@{
ViewData["Title"] = "Change password";
ViewData.AddActivePage(ManageNavPages.ChangePassword);
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Change password");
}
<h4>@ViewData["Title"]</h4>
@ -25,7 +24,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Update password</button>
<button type="submit" class="btn btn-primary">Update password</button>
</form>
</div>
</div>

@ -1,13 +1,12 @@
@{
ViewData["Title"] = "Disable two-factor authentication (2FA)";
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Disable two-factor authentication (2FA)");
}
<h2>@ViewData["Title"]</h2>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<span class="fa fa-warning"></span>
<strong>This action only disables 2FA.</strong>
</p>
<p>

@ -1,7 +1,6 @@
@model EnableAuthenticatorViewModel
@{
ViewData["Title"] = "Enable authenticator";
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Enable authenticator");
}
<h4>@ViewData["Title"]</h4>
@ -37,7 +36,7 @@
<input asp-for="Code" class="form-control" autocomplete="off" />
<span asp-validation-for="Code" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Verify</button>
<button type="submit" class="btn btn-primary">Verify</button>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
</form>
</div>

@ -1,27 +1,26 @@
@model ExternalLoginsViewModel
@{
ViewData["Title"] = "Manage your external logins";
ViewData.AddActivePage(ManageNavPages.ExternalLogins);
ViewData.SetActivePageAndTitle(ManageNavPages.ExternalLogins, "Manage your external logins");
}
@Html.Partial("_StatusMessage", Model.StatusMessage)
@if(Model.CurrentLogins?.Count > 0)
@if (Model.CurrentLogins?.Count > 0)
{
<h4>Registered Logins</h4>
<table class="table">
<table class="table table-sm table-responsive-md">
<tbody>
@foreach(var login in Model.CurrentLogins)
@foreach (var login in Model.CurrentLogins)
{
<tr>
<td>@login.LoginProvider</td>
<td>
@if(Model.ShowRemoveButton)
@if (Model.ShowRemoveButton)
{
<form asp-action="RemoveLogin" method="post">
<div>
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" />
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" />
<button type="submit" class="btn btn-default" title="Remove this @login.LoginProvider login from your account">Remove</button>
<button type="submit" class="btn btn-primary" title="Remove this @login.LoginProvider login from your account">Remove</button>
</div>
</form>
}
@ -35,16 +34,16 @@
</tbody>
</table>
}
@if(Model.OtherLogins?.Count > 0)
@if (Model.OtherLogins?.Count > 0)
{
<h4>Add another service to log in.</h4>
<hr />
<form asp-action="LinkLogin" method="post" class="form-horizontal">
<div id="socialLoginList">
<p>
@foreach(var provider in Model.OtherLogins)
@foreach (var provider in Model.OtherLogins)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>

@ -1,13 +1,12 @@
@model GenerateRecoveryCodesViewModel
@{
ViewData["Title"] = "Recovery codes";
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Recovery codes");
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<span class="fa fa-warning"></span>
<strong>Put these codes in a safe place.</strong>
</p>
<p>

@ -1,7 +1,6 @@
@model IndexViewModel
@{
ViewData["Title"] = "Profile";
ViewData.AddActivePage(ManageNavPages.Index);
ViewData.SetActivePageAndTitle(ManageNavPages.Index, "Profile");
}
<h4>@ViewData["Title"]</h4>
@ -25,7 +24,7 @@
{
<div class="input-group">
<input asp-for="Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
<span class="input-group-addon" aria-hidden="true"><span class="fa fa-check text-success"></span></span>
</div>
}
else
@ -35,7 +34,7 @@
}
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Save</button>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>

@ -2,41 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Views.Manage
{
public static class ManageNavPages
public enum ManageNavPages
{
public static string ActivePageKey => "ActivePage";
public static string Index => "Index";
public static string ChangePassword => "ChangePassword";
public static string ExternalLogins => "ExternalLogins";
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
public static string Tokens => "Tokens";
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
public static string PageNavClass(ViewContext viewContext, string page)
{
var activePage = viewContext.ViewData["ActivePage"] as string;
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
}
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
Index, ChangePassword, ExternalLogins, TwoFactorAuthentication, Tokens
}
}

@ -1,12 +1,11 @@
@{
ViewData["Title"] = "Reset authenticator key";
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Reset authenticator key");
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<span class="fa fa-warning"></span>
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
</p>
<p>

@ -1,7 +1,6 @@
@model SetPasswordViewModel
@{
ViewData["Title"] = "Set password";
ViewData.AddActivePage(ManageNavPages.ChangePassword);
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Set password");
}
<h4>Set your password</h4>
@ -24,7 +23,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Set password</button>
<button type="submit" class="btn btn-primary">Set password</button>
</form>
</div>
</div>

@ -1,7 +1,6 @@
@model TwoFactorAuthenticationViewModel
@{
ViewData["Title"] = "Two-factor authentication";
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Two-factor authentication");
}
<h4>@ViewData["Title"]</h4>
@ -29,19 +28,19 @@
</div>
}
<a asp-action="Disable2faWarning" class="btn btn-default">Disable 2FA</a>
<a asp-action="GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
<a asp-action="Disable2faWarning" class="btn btn-primary">Disable 2FA</a>
<a asp-action="GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
}
<h5>Authenticator app</h5>
@if(!Model.HasAuthenticator)
{
<a asp-action="EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
<a asp-action="EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
}
else
{
<a asp-action="EnableAuthenticator" class="btn btn-default">Configure authenticator app</a>
<a asp-action="ResetAuthenticatorWarning" class="btn btn-default">Reset authenticator key</a>
<a asp-action="EnableAuthenticator" class="btn btn-primary">Configure authenticator app</a>
<a asp-action="ResetAuthenticatorWarning" class="btn btn-primary">Reset authenticator key</a>
}
@section Scripts {

@ -1,16 +1,15 @@
@using BTCPayServer.Views.Manage
@inject SignInManager<ApplicationUser> SignInManager
@inject SignInManager<ApplicationUser> SignInManager
@{
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
<ul class="nav nav-pills nav-stacked">
<li class="@ManageNavPages.IndexNavClass(ViewContext)"><a asp-action="Index">Profile</a></li>
<li class="@ManageNavPages.ChangePasswordNavClass(ViewContext)"><a asp-action="ChangePassword">Password</a></li>
@if(hasExternalLogins)
<div class="nav flex-column nav-pills">
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
@if (hasExternalLogins)
{
<li class="@ManageNavPages.ExternalLoginsNavClass(ViewContext)"><a asp-action="ExternalLogins">External logins</a></li>
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ExternalLogins)" asp-action="ExternalLogins">External logins</a>
}
<li class="@ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)"><a asp-action="TwoFactorAuthentication">Two-factor authentication</a></li>
</ul>
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
</div>

@ -1,7 +1,6 @@
@model EmailsViewModel
@{
ViewData["Title"] = ServerNavPages.Emails;
ViewData.AddActivePage(ServerNavPages.Emails);
ViewData.SetActivePageAndTitle(ServerNavPages.Emails);
}
@ -56,8 +55,8 @@
<input asp-for="TestEmail" class="form-control" />
</div>
<button type="submit" class="btn btn-success" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-default" name="command" value="Test">Test</button>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-primary" name="command" value="Test">Test</button>
</form>
</div>
</div>

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