Compare commits

...

160 Commits

Author SHA1 Message Date
65dcfd3549 bump 2019-04-15 15:28:05 +09:00
6976fc54ca Merge pull request #765 from Kukks/bugfix/crowdfund
Fix dynamic  crowdfund labelling
2019-04-15 15:26:58 +09:00
0e077ff5c4 Merge pull request #768 from Kukks/feature/invoicesearchsession
Make invoice list search term persistent for session
2019-04-15 15:26:05 +09:00
c2f171a729 Merge pull request #766 from Kukks/bugfix/crowdfund_orderid
fix redirect uri for crowdfund invoices
2019-04-15 15:24:53 +09:00
fea38758e4 Merge pull request #767 from Kukks/bugfix/unusual-filter
fix unusual filter
2019-04-15 15:24:41 +09:00
444733565b Updating altcoins section 2019-04-13 22:24:46 -05:00
96d28f00cc Make invoice list search term persistent for session 2019-04-13 14:00:48 +02:00
70cc79a77f fix unusual filter
closes #763
2019-04-13 13:50:14 +02:00
8d10186fdf fix redirect uri for crowdfund invoices
closes #759
2019-04-13 13:43:47 +02:00
6f7e0205f8 Fix dynamic crowdfund labelling
closes #760
2019-04-13 13:22:19 +02:00
7ef11817c1 Add britt and rockstar video in readme 2019-04-12 18:03:05 +09:00
c387c84861 bump 2019-04-12 15:02:28 +09:00
ae7ad9f667 Filter the apps by the user id 2019-04-12 14:54:59 +09:00
c55f1185e6 Revert "Do not show all apps in Server settings policy"
This reverts commit 1619666befd4bf8931a11a8c2927de4c5b1add86.
2019-04-12 14:43:07 +09:00
1619666bef Do not show all apps in Server settings policy 2019-04-12 14:29:56 +09:00
bf784f6fd7 Merge pull request #758 from rockstardev/dynamicroot
Allowing for displaying of app directly on website root
2019-04-12 14:19:19 +09:00
13e330fa65 Better UI for selection of app to be displayed on root 2019-04-12 00:13:14 -05:00
827b133534 Allowing for displaying of app directly on website root 2019-04-11 16:30:23 -05:00
4067d4b00f Remove the Facade concept 2019-04-11 23:55:20 +09:00
359d8c5c6a Merge pull request #745 from Kukks/feature/invoicepaymentdata
Add payment data to crypto info in invoice api model
2019-04-11 19:10:52 +09:00
265b7364e8 Merge pull request #756 from Kukks/invoice-auto-redirect
Allow POS to redirect invoices automatically after paid
2019-04-11 19:10:21 +09:00
dc2b8c9e4c bump to nbitpay and use for payments 2019-04-11 12:00:10 +02:00
37869fd049 Add payment data to crypto info in invoice api model
Depends on https://github.com/MetacoSA/NBitpayClient/pull/22
2019-04-11 11:54:56 +02:00
d7ada4d493 add redirect automatically to checkout experience/ store settings 2019-04-11 11:53:31 +02:00
f093f85dbf Merge pull request #753 from britttttk/fix/defaultText
Improve default payment method dropdown
2019-04-11 18:11:46 +09:00
1cf17872ab Allow POS to redirect invoices automatically after paid
closes #730
2019-04-11 11:08:42 +02:00
c79751829b Merge pull request #754 from Kukks/fix-pos-notif
fix pos settings savings for notifications
2019-04-11 17:34:42 +09:00
7a21c03896 fix pos settings savings for notifications
closes #751
2019-04-11 09:14:39 +02:00
54f07139db Improve default payment method dropdown text 2019-04-11 00:41:30 -06:00
d78990fbd5 Changing queries: Using FirstOrDefaultAsync result in suboptimal queries 2019-04-11 15:05:30 +09:00
9ed7dbc838 Remove Quadrigacx (bankrupt) 2019-04-11 14:57:31 +09:00
9b12c7bc57 Add missing file 2019-04-11 12:41:38 +09:00
8973c75bbc fix build 2019-04-11 10:06:36 +09:00
f425df7b6d Add Contributing section 2019-04-10 16:05:07 -05:00
a44b600c5e Improving documentation and altcoins sections 2019-04-10 12:48:02 -05:00
7f0a42c2d5 Updating altcoins section 2019-04-10 12:30:54 -05:00
60cd864226 Inject HttpClient inside lightning client instances 2019-04-11 01:10:46 +09:00
71cf02915e Merge pull request #740 from rockstardev/uifixes
Button to switch between time formats, width fix
2019-04-09 18:05:18 +09:00
327d2298fb Merge pull request #746 from Kukks/tag-pos-invoices
tag pos invoices too
2019-04-09 18:04:14 +09:00
2ca11ed692 Fix PoS decimal issue (Fix #747) 2019-04-09 11:10:27 +09:00
0224815a60 workaround tight coupling of crowdfund to apps mechanics 2019-04-08 16:02:53 +02:00
df824c36d2 tag pos invoices too 2019-04-08 15:46:24 +02:00
66e7777b1a bump nbx 2019-04-08 22:26:52 +09:00
7ff85a86bf Merge pull request #736 from Dolu89/master
New Pay Button type (Custom amount and Slider)
2019-04-08 22:19:05 +09:00
7b3700c2c6 Fix bitbank API weirdness (Fix #741) 2019-04-08 21:57:12 +09:00
04679aefd6 Merge pull request #743 from Kukks/fix-coinswitch
fix coinswitch
2019-04-08 17:23:47 +09:00
5190639b77 Simplify InvoiceWatcher logic and remove unused code 2019-04-08 13:28:13 +09:00
0bf73abb39 Fix Custom amount under 0 in Pay button 2019-04-06 15:22:09 +02:00
7e0211924d Replace inline js by templates in pay button 2019-04-06 15:02:02 +02:00
a8a857a7ce Move Slider settings below radio buttons 2019-04-06 13:47:22 +02:00
1d18965a26 fix coinswitch 2019-04-06 08:10:27 +02:00
e020b86a3f Button to switch between time formats, width fix 2019-04-05 09:44:49 -05:00
1b80b90609 Update lang, bump 2019-04-05 18:50:20 +09:00
efc3512994 Merge pull request #739 from Kukks/pos-fixes
fix malformed html in pos + align price/button to card bottom
2019-04-05 16:53:33 +09:00
acb8ca982f fix malformed html in pos + align price/button to card bottom 2019-04-05 09:50:41 +02:00
adc42cbba4 Put timeout on tests 2019-04-05 16:28:18 +09:00
7edcb7ef5f Update NBitcoin 2019-04-05 16:21:00 +09:00
656017c6df SocketFactory uses NBitcoin implementation of Socks 2019-04-05 16:19:04 +09:00
35db6d4a8b Fix test CanScheduleBackgroundTasks 2019-04-05 15:32:30 +09:00
2741187546 Merge pull request #737 from rockstardev/uifixes
New batch of UI fixes
2019-04-05 15:23:41 +09:00
c3a7ab647c Increase reliability of test CanScheduleBackgroundTasks 2019-04-05 15:16:36 +09:00
92da0ec2d2 fix tests 2019-04-05 14:59:46 +09:00
c767a49f2d Increase notitifcation timeout to 1 minute, make sure that BackgroundJobScheduler is correctly cancelling tasks 2019-04-05 14:58:25 +09:00
ea8196b532 Do not use HttpClient singleton for the InvoiceNotifcationManager 2019-04-05 14:31:09 +09:00
58f138e854 Invoice list improvements
Items 5 and 9 from #349
2019-04-04 22:25:38 -05:00
b4b6939498 Coin switching on no-script invoices 2019-04-04 21:48:52 -05:00
bc97c07670 Add currency select in Pay Button 2019-04-04 21:32:16 +02:00
cf27fe5a53 Merge remote-tracking branch 'upstream/master' 2019-04-04 20:57:16 +02:00
449066449b Add a new Pay Button Type : Slider 2019-04-04 20:56:12 +02:00
eb5e32a07f Remove exception thrown by binance provider 2019-04-04 19:39:37 +09:00
708cdbe23f Remove bunch of catched exception when BTCPay starts 2019-04-04 19:35:46 +09:00
333de52c33 Merge branch 'feature/sync-video' 2019-04-04 18:17:39 +09:00
6b9932fa14 Escape css selector 2019-04-04 18:16:54 +09:00
6c45689e6a Fix point of sale search (Fix #734) 2019-04-04 16:32:56 +09:00
1e3307c84c Add link to andreas video during IBD 2019-04-04 15:58:28 +09:00
d0eed9857d Prevent user to log in or register via unsecured network 2019-04-04 14:28:50 +09:00
4221763f48 Merge remote-tracking branch 'upstream/master' 2019-04-03 21:45:41 +02:00
184c797b0e Add a new Pay Button Type : Custom amount 2019-04-03 21:43:53 +02:00
4853e15d8a Better timing measurement during invoice creation 2019-04-03 15:00:09 +09:00
6b4b903669 Improve invoice logs, make sure logs are saved as fire and forget 2019-04-03 14:38:35 +09:00
05da63f2a5 Merge pull request #721 from Kukks/expose-notif-to-pos
add notif Email to crowdfund and pos + add notif url to pos
2019-04-02 17:34:56 +09:00
5b4b073fc8 Merge pull request #731 from Kukks/coinswitch-shitcoin-tax
add coinswitch shitcoin tax
2019-04-02 17:33:37 +09:00
4723a83dbb Merge pull request #732 from rockstardev/uifixes
UI fixes
2019-04-02 17:32:47 +09:00
78350db62d Add a Clean button in the Maintenance page 2019-04-01 17:10:05 +09:00
e8b71f36b2 Adding support for noscript invoices 2019-03-31 13:46:38 -05:00
6db9061dd1 add coinswitch shitcoin tax 2019-03-31 18:55:14 +02:00
320826a4b9 Returning empty payload to fix JSON parse error thrown in JS 2019-03-31 11:48:53 -05:00
ad0edb5f4c Make sure arm build have /sbin/ip 2019-03-31 14:08:53 +09:00
2856c10bc3 Revert "Do not use /sbin/ip to fetch the current ip (fix #712)"
This reverts commit 561644f75b3b83cae3b1cc8db0ceb7ee657dbe82.
2019-03-31 13:54:10 +09:00
24aa18e9ed bump 2019-03-31 13:32:26 +09:00
767eca97cb Fix tests 2019-03-31 13:31:50 +09:00
73d5415ea9 Use NBitcoin's socks implementation 2019-03-31 13:16:05 +09:00
e5a26cfca8 Update dependencies 2019-03-31 12:08:08 +09:00
e40cd1fc0c Update publish docker 2019-03-29 18:39:02 +09:00
978b7d930e Catch operation cancelled exception on the BackgroundJobScheduler 2019-03-29 18:09:54 +09:00
0f2e3ef957 Make latest branch 2019-03-29 17:48:24 +09:00
275b590e80 add notif Email to crowdfund and pos + add notif url to pos
closes #720
2019-03-29 07:51:00 +01:00
5d9da82d8e fix build 2019-03-27 18:58:56 +09:00
1a122726b7 Add more timeout for lightning tests 2019-03-27 18:57:51 +09:00
0bd02a9272 Fix some exceptions raised if port is already used 2019-03-27 18:56:43 +09:00
3cce7b8b35 Refactor the lightning listener, some users complain payments are not detected (should fix #676) 2019-03-27 15:53:38 +09:00
e3ab1f5228 Merge pull request #707 from Kukks/user-email-sync
set username on email change
2019-03-25 14:59:02 +09:00
4c875d9c7c update doc sdk 2019-03-25 14:10:16 +09:00
e79334a6f6 Fix: if anyone can create invoice and /invoices has storeId parameters, then it should be allowed 2019-03-25 12:59:42 +09:00
a09c6d51e6 fix exception which can be thrown if the store is not found 2019-03-25 12:24:48 +09:00
312c7b7193 Fix anonymous bitpay api access 2019-03-25 12:22:17 +09:00
ee733fee28 If AnyoneCanInvoice and the storeId is passed as a parameter to the Bitpay API, then allow request 2019-03-25 12:18:39 +09:00
4d7e9d3f8a Rewrite the BitpayAuthHandler more clearly 2019-03-25 12:09:18 +09:00
873c0a183a Merge pull request #713 from pavlenex/link
Update Readme, Fix Broken Pebble Link
2019-03-25 10:20:08 +09:00
ea53ae8f20 Update ManageController.cs 2019-03-24 16:09:36 +01:00
686bc3380d Update ManageController.cs 2019-03-24 16:09:20 +01:00
67da20bcea Merge pull request #706 from Kukks/pos-max-length
remove template max length in pos app
2019-03-24 23:58:08 +09:00
561644f75b Do not use /sbin/ip to fetch the current ip (fix #712) 2019-03-24 23:56:31 +09:00
1abc89858f Fix broken Pebble Link
Fix broken Pebble Link
2019-03-24 14:15:47 +01:00
91c63a8ee6 bump 2019-03-24 13:37:14 +09:00
563882d30b Merge pull request #711 from rockstardev/bugfix/satround
Javascript floating point math fix, closes #701
2019-03-24 13:35:13 +09:00
9a5eeee794 Javascript floating point math fix, closes #701 2019-03-23 22:28:54 -05:00
0578a692db Updating bundler to new version that will also support .NET Core 2.2 2019-03-23 19:10:16 -05:00
f74f06338a Update bundle minifier, trying to fix (#710 with bundle on) 2019-03-23 23:24:29 +09:00
1281f348bf set username on email change
closes #673
2019-03-22 12:53:56 +01:00
5e76d4bfc1 remove template max length in pos app
closes #704
2019-03-22 09:14:27 +01:00
2a302ea346 Do not spam logs if we can't connect to lightning because lightning is not started 2019-03-20 14:35:33 +09:00
be90172840 bump Microsoft.AspNetCore.App 2019-03-20 14:13:12 +09:00
b85ee895f5 Update nbxplorer in the tests 2019-03-20 14:08:25 +09:00
93de408e07 bump 2019-03-20 12:37:53 +09:00
d3662ae734 fix dockerfile 2019-03-20 12:34:47 +09:00
132d7795ea bump .net 2019-03-20 12:22:46 +09:00
bf5a624209 Remove curl dependency on alpine image 2019-03-20 11:49:43 +09:00
abbdbda03a Add Bitbank provider 2019-03-20 00:49:44 +09:00
8a8593437a Add TOR
- Adds Tor
- Clarifies alts (once again)
- adds payment requests
2019-03-18 12:00:31 +01:00
e203cada54 Merge pull request #675 from Kukks/css-grid-pos-fix
attempt fix pos css grid
2019-03-18 19:47:11 +09:00
00c11c7ee9 bump 2019-03-18 18:58:15 +09:00
82126b85d2 Fix tests 2019-03-18 18:52:19 +09:00
4f428c8ed1 Fix other tor services display 2019-03-18 17:13:02 +09:00
9868af4db8 Show onion link in the navbar 2019-03-18 17:07:39 +09:00
0a5d7c5efa Add .local as a localnetwork link 2019-03-18 16:48:04 +09:00
c2754b324d Show other tor services 2019-03-18 16:45:46 +09:00
5c618233cb fix singular item in pos stretched width 2019-03-18 07:39:45 +01:00
9e91259b9e attempt fix pos css grid 2019-03-18 07:07:31 +01:00
014d08f38a Add socks support on BTCPay Server to query onion endpoints 2019-03-18 00:03:02 +09:00
7998ea142b Fix tor services not appearing 2019-03-17 21:47:08 +09:00
a4051dac72 Make sure BTCPay show TOR lightning node info if the site is accessed through TOR 2019-03-17 21:28:47 +09:00
e3a8892d24 Check tor services in the background 2019-03-17 21:07:24 +09:00
ea02d77e69 Parse torrc file to know virtual port of hidden services 2019-03-17 20:49:26 +09:00
4f582a6712 Show onion hosts instead of url (can't know about ports) 2019-03-17 13:42:54 +09:00
4769b1d452 Show tor services inside hidden directories in Server Settings/Services 2019-03-17 13:01:47 +09:00
17b18d820f bump 2019-03-17 12:28:38 +09:00
26f34e75c2 Language update 2019-03-17 12:28:19 +09:00
6f50ac50ec Merge pull request #689 from rockstardev/rock-lnsats
Allowing for lightning payment amounts to be displayed in Satoshis
2019-03-17 12:26:13 +09:00
5261cfcdd3 Allowing for lightning payment amounts to be displayed in Satoshis 2019-03-15 22:43:57 -05:00
675920697f Merge pull request #685 from britttttk/fix/PayReqMessage
Improve delete payment request message
2019-03-16 12:36:24 +09:00
24699bf2ba Updating QR Code on public node info page to be SVG
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2019-03-15 22:16:12 -05:00
5ab92ed794 Switching to SVG for QR code so it looks OK on higher density display
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2019-03-15 21:45:55 -05:00
8e83f0faa1 Improve delete payment request message 2019-03-14 23:26:39 -06:00
30d5add2ea bump 2019-03-15 14:04:17 +09:00
6e47babf45 Overpayment of lightning invoice now properly appears (Fix #486) 2019-03-14 18:48:05 +09:00
9b95fa1f20 Add links for Payment Requests and Apps docs in UI 2019-03-12 21:50:55 +09:00
c67aa14a87 Fix payment requests and crowdfund app not working if ROOTPATH is specified (fix #659) 2019-03-12 15:48:24 +09:00
23f296ef34 The store owner can define default currency pairs when using rate API without parameter 2019-03-11 18:39:21 +09:00
c6ce676ad3 Fix exception on Payment Request (#672) 2019-03-11 16:06:27 +09:00
131 changed files with 2666 additions and 1204 deletions

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

View File

@ -35,6 +35,7 @@ using System.Text;
using System.Threading;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
namespace BTCPayServer.Tests
{
@ -120,7 +121,8 @@ namespace BTCPayServer.Tests
File.WriteAllText(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
_Host = new WebHostBuilder()
@ -209,6 +211,8 @@ namespace BTCPayServer.Tests
}
}
public HttpClient HttpClient { get; set; }
public string HostName
{
get;

View File

@ -1,12 +1,9 @@
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
RUN apk add --no-cache curl
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
@ -35,27 +36,29 @@ namespace BTCPayServer.Tests
}
}
public void Advance(TimeSpan time)
public async Task Advance(TimeSpan time)
{
_Now += time;
List<WaitObj> overdue = new List<WaitObj>();
lock (waits)
{
foreach (var wait in waits.ToArray())
{
if (_Now >= wait.Expiration)
{
wait.CTS.TrySetResult(true);
overdue.Add(wait);
waits.Remove(wait);
}
}
}
foreach (var o in overdue)
o.CTS.TrySetResult(true);
try
{
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
}
catch { }
}
public void AdvanceMilliseconds(long milli)
{
Advance(TimeSpan.FromMilliseconds(milli));
}
public override string ToString()
{
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);

View File

@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanPayPaymentRequestWhenPossible()
{

View File

@ -23,6 +23,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{
@ -85,9 +86,11 @@ namespace BTCPayServer.Tests
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public Task EnsureChannelsSetup()
public async Task EnsureChannelsSetup()
{
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
Logs.Tester.LogInformation("Connecting channels");
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
Logs.Tester.LogInformation("Channels connected");
}
private IEnumerable<ILightningClient> GetLightningSenderClients()

View File

@ -55,6 +55,7 @@ using BTCPayServer.Events;
using BTCPayServer.Configuration;
using System.Security;
using System.Runtime.CompilerServices;
using System.Net;
namespace BTCPayServer.Tests
{
@ -146,6 +147,45 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseTorrc()
{
var nl = "\n";
var input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:81";
nl = Environment.NewLine;
var expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"HiddenServicePort 80 172.19.0.10:81" + nl;
Assert.True(Torrc.TryParse(input, out var torrc));
Assert.Equal(expected, torrc.ToString());
nl = "\r\n";
input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:81";
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:80" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.11:80";
nl = Environment.NewLine;
expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"HiddenServicePort 80 172.19.0.10:80" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
"HiddenServicePort 80 172.19.0.11:80" + nl;
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
@ -361,7 +401,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 1000)]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetLightningServer()
{
@ -399,21 +439,21 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCLightning()
{
await ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCharge()
{
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact(Timeout = 60 * 1000)]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentLnd()
{
@ -453,7 +493,9 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description"
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
Logs.Tester.LogInformation($"Trying to send Lightning payment to {invoice.Id}");
await tester.SendLightningPaymentAsync(invoice);
Logs.Tester.LogInformation($"Lightning payment to {invoice.Id} is sent");
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
@ -476,7 +518,6 @@ namespace BTCPayServer.Tests
var controller = acc.GetController<StoresController>();
var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "bla",
PublicKey = null
}).GetAwaiter().GetResult();
@ -727,6 +768,18 @@ namespace BTCPayServer.Tests
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// We don't have any default currencies, so this should be failing
Assert.Null(GetRatesResult?.Data);
var store = acc.GetController<StoresController>();
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates(acc.StoreId)).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
store.Rates(ratesVM).Wait();
store = acc.GetController<StoresController>();
rateController = acc.GetController<RateController>();
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// Now we should have a result
Assert.NotNull(GetRatesResult);
Assert.NotNull(GetRatesResult.Data);
Assert.Equal(2, GetRatesResult.Data.Length);
@ -869,7 +922,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
foreach(var req in new[]
foreach (var req in new[]
{
"invoices/",
"invoices",
@ -918,7 +971,6 @@ namespace BTCPayServer.Tests
var storeController = user.GetController<StoresController>();
storeController.CreateToken(new CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "test2",
StoreId = user.StoreId
}).GetAwaiter().GetResult();
@ -1006,7 +1058,7 @@ namespace BTCPayServer.Tests
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
vm.PreferredExchange = exchange;
storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@ -1021,6 +1073,49 @@ namespace BTCPayServer.Tests
return invoice2.CryptoInfo[0].Rate;
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseAnyoneCanCreateInvoice()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Logs.Tester.LogInformation("StoreId without anyone can create invoice = 401");
var response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(401, (int)response.StatusCode);
Logs.Tester.LogInformation("No store without anyone can create invoice = 404 because the bitpay API can't know the storeid");
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(404, (int)response.StatusCode);
user.ModifyStore(s => s.AnyoneCanCreateInvoice = true);
Logs.Tester.LogInformation("Bad store with anyone can create invoice = 401");
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId=badid")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(401, (int)response.StatusCode);
Logs.Tester.LogInformation("Good store with anyone can create invoice = 200");
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(200, (int)response.StatusCode);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanTweakRate()
@ -1044,7 +1139,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@ -1143,7 +1238,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var store = user.GetController<StoresController>();
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange);
Assert.Equal(0.0, rateVm.Spread);
@ -1151,7 +1246,7 @@ namespace BTCPayServer.Tests
rateVm.PreferredExchange = "bitflyer";
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
@ -1168,7 +1263,8 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal(rateVm.StoreId, user.StoreId);
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
Assert.True(rateVm.ShowScripting);
rateVm.ScriptTest = "BTC_JPY";
@ -1185,7 +1281,7 @@ namespace BTCPayServer.Tests
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
@ -1457,7 +1553,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 1000)]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
@ -1612,21 +1708,20 @@ donation:
[Fact]
[Trait("Fast", "Fast")]
public void CanScheduleBackgroundTasks()
public async Task CanScheduleBackgroundTasks()
{
BackgroundJobClient client = new BackgroundJobClient();
MockDelay mockDelay = new MockDelay();
client.Delay = mockDelay;
bool[] jobs = new bool[4];
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
client.Schedule((_) => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(5.0));
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
client.Schedule((_) => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(2.0));
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
client.Schedule((_) => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
client.Schedule((_) => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(7.0));
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
CancellationTokenSource cts = new CancellationTokenSource();
@ -1636,52 +1731,52 @@ donation:
var waitJobsFinish = client.WaitAllRunning(default);
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
await mockDelay.Advance(TimeSpan.FromSeconds(2.0));
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
await mockDelay.Advance(TimeSpan.FromSeconds(3.0));
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
Assert.Equal(1, client.GetExecutingCount());
Assert.False(waitJobsFinish.Wait(100));
Assert.False(waitJobsFinish.Wait(1));
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
Assert.True(waitJobsFinish.Wait(100));
await waitJobsFinish;
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.Equal(0, client.GetExecutingCount());
bool jobExecuted = false;
Logs.Tester.LogInformation("This job will be cancelled");
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
client.Schedule((_) => { jobExecuted = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(1.0));
await mockDelay.Advance(TimeSpan.FromSeconds(0.5));
Assert.False(jobExecuted);
Thread.Sleep(100);
Assert.Equal(1, client.GetExecutingCount());
TestUtils.Eventually(() => Assert.Equal(1, client.GetExecutingCount()));
waitJobsFinish = client.WaitAllRunning(default);
Assert.False(waitJobsFinish.Wait(100));
cts.Cancel();
Assert.True(waitJobsFinish.Wait(1000));
await waitJobsFinish;
Assert.True(waitJobsFinish.Wait(1));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.False(waitJobsFinish.IsFaulted);
Assert.False(jobExecuted);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Thread.Sleep(100); // Make sure it get cancelled
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.False(jobExecuted);
Assert.Equal(0, client.GetExecutingCount());
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await processing);
Assert.True(processing.IsCanceled);
Assert.True(client.WaitAllRunning(default).Wait(100));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
}
[Fact]
@ -2251,15 +2346,22 @@ donation:
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
if (result.ExpectedName == "bitbank")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
}
else
{
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
@ -2352,7 +2454,7 @@ donation:
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
// Error if directory not exists
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));

View File

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.15
image: nicolasdorier/nbxplorer:2.0.0.27
restart: unless-stopped
ports:
- "32838:32838"

View File

@ -8,10 +8,6 @@ namespace BTCPayServer.Authentication
{
public class BitTokenEntity
{
public string Facade
{
get; set;
}
public string Value
{
get; set;
@ -39,7 +35,6 @@ namespace BTCPayServer.Authentication
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,

View File

@ -11,11 +11,6 @@ namespace BTCPayServer.Authentication
get;
set;
}
public string Facade
{
get;
set;
}
public string Label
{
get;

View File

@ -90,7 +90,6 @@ namespace BTCPayServer.Authentication
return new BitTokenEntity()
{
Label = data.Label,
Facade = data.Facade,
Value = data.Id,
SIN = data.SIN,
PairingTime = data.PairingTime,
@ -129,7 +128,6 @@ namespace BTCPayServer.Authentication
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode);
}
@ -178,7 +176,6 @@ namespace BTCPayServer.Authentication
{
Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade,
Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
@ -213,7 +210,6 @@ namespace BTCPayServer.Authentication
return null;
return new PairingCodeEntity()
{
Facade = data.Facade,
Id = data.Id,
Label = data.Label,
Expiration = data.Expiration,

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.82</Version>
<Version>1.0.3.94</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -30,25 +30,27 @@
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.11" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.16" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.86" />
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
<PackageReference Include="NBitcoin" Version="4.1.1.98" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.5" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.6" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
@ -67,7 +69,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>

View File

@ -48,11 +48,7 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public EndPoint SocksEndpoint { get; set; }
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
@ -149,6 +145,16 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
if(!string.IsNullOrEmpty(socksEndpointString))
{
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
throw new ConfigException("Invalid value for socksendpoint");
SocksEndpoint = endpoint;
}
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -273,5 +279,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public string TorrcFile { get; set; }
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using NBitcoin;
namespace BTCPayServer.Configuration
{

View File

@ -40,6 +40,8 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);

View File

@ -42,15 +42,12 @@ namespace BTCPayServer.Controllers
{
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required");
if (string.IsNullOrEmpty(request.Facade))
throw new BitpayHttpException(400, "'facade' property is required");
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = pairingCode,
Facade = request.Facade,
Label = request.Label
});
@ -86,7 +83,7 @@ namespace BTCPayServer.Controllers
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = pairingEntity.Facade,
Facade = "merchant",
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}

View File

@ -136,6 +136,7 @@ namespace BTCPayServer.Controllers
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,

View File

@ -77,6 +77,9 @@ namespace BTCPayServer.Controllers
public string CustomCSSLink { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
public bool? RedirectAutomatically { get; set; }
}
[HttpGet]
@ -101,7 +104,10 @@ namespace BTCPayServer.Controllers
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink
CustomCSSLink = settings.CustomCSSLink,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
};
if (HttpContext?.Request != null)
{
@ -174,7 +180,11 @@ namespace BTCPayServer.Controllers
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
});
await UpdateAppSettings(app);
StatusMessage = "App updated";

View File

@ -7,8 +7,10 @@ using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
@ -31,16 +33,19 @@ namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppService AppService,
public AppsPublicController(AppService AppService,
BTCPayServerOptions btcPayServerOptions,
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppService = AppService;
_BtcPayServerOptions = btcPayServerOptions;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private AppService _AppService;
private readonly BTCPayServerOptions _BtcPayServerOptions;
private InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
@ -85,34 +90,34 @@ namespace BTCPayServer.Controllers
AppId = appId
});
}
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
{
if(!isAdmin)
if (!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
if (settings.Enabled) return View(appInfo);
if(!isAdmin)
if (settings.Enabled)
return View(appInfo);
if (!isAdmin)
return NotFound();
return View(appInfo);
@ -134,7 +139,8 @@ namespace BTCPayServer.Controllers
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin) {
if (!settings.Enabled && !isAdmin)
{
return NotFound("Crowdfund is not currently active");
}
@ -176,22 +182,26 @@ namespace BTCPayServer.Controllers
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) }, cancellationToken: cancellationToken);
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
NotificationEmail = settings.NotificationEmail,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ??
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string> {AppService.GetAppInternalTag(appId)},
cancellationToken: cancellationToken);
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
new { invoiceId = invoice.Data.Id });
}
else
{
@ -202,7 +212,7 @@ namespace BTCPayServer.Controllers
{
return BadRequest(e.Message);
}
}
[HttpPost]
@ -211,7 +221,7 @@ namespace BTCPayServer.Controllers
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
string email,
string orderId,
string notificationUrl,
@ -262,15 +272,21 @@ namespace BTCPayServer.Controllers
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
NotificationEmail = settings.NotificationEmail,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -8,19 +8,39 @@ using System.Net.Http;
using Newtonsoft.Json.Linq;
using NBitcoin;
using Newtonsoft.Json;
using BTCPayServer.Services;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Controllers
{
public class HomeController : Controller
{
private readonly CssThemeManager _cachedServerSettings;
public IHttpClientFactory HttpClientFactory { get; }
public HomeController(IHttpClientFactory httpClientFactory)
public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings)
{
HttpClientFactory = httpClientFactory;
_cachedServerSettings = cachedServerSettings;
}
public IActionResult Index()
public async Task<IActionResult> Index()
{
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
}
return View("Home");
}

View File

@ -19,6 +19,7 @@ using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
@ -188,7 +189,7 @@ namespace BTCPayServer.Controllers
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
//
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
@ -213,6 +214,23 @@ namespace BTCPayServer.Controllers
return View(nameof(Checkout), model);
}
[HttpGet]
[Route("invoice-noscript")]
public async Task<IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
//
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return View(model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
@ -284,6 +302,7 @@ namespace BTCPayServer.Controllers
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
@ -296,6 +315,7 @@ namespace BTCPayServer.Controllers
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
@ -315,6 +335,7 @@ namespace BTCPayServer.Controllers
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage?? 0,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
@ -437,7 +458,7 @@ namespace BTCPayServer.Controllers
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok();
return Ok("{}");
}
[HttpGet]
@ -446,6 +467,10 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
if (searchTerm == null)
{
searchTerm = HttpContext.Session.GetString("InvoicesSearchTerm");
}
var model = new InvoicesModel
{
SearchTerm = searchTerm,
@ -607,6 +632,14 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices)
{
if (invoices.SearchTerm == null)
{
HttpContext.Session.Remove("InvoicesSearchTerm");
}
else
{
HttpContext.Session.SetString("InvoicesSearchTerm", invoices.SearchTerm);
}
return RedirectToAction(nameof(ListInvoices), new
{
searchTerm = invoices.SearchTerm,

View File

@ -84,6 +84,7 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
if (invoice.NotificationURL != null &&
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
@ -105,7 +106,7 @@ namespace BTCPayServer.Controllers
}
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
@ -125,6 +126,9 @@ namespace BTCPayServer.Controllers
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
entity.Status = InvoiceStatus.New;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
@ -138,7 +142,7 @@ namespace BTCPayServer.Controllers
.Where(c => c.Value.Enabled)
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
.ToHashSet();
excludeFilter = PaymentFilter.Or(excludeFilter,
excludeFilter = PaymentFilter.Or(excludeFilter,
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
}
@ -200,8 +204,22 @@ namespace BTCPayServer.Controllers
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
}
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
using (logs.Measure("Saving invoice"))
{
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
}
_ = Task.Run(async () =>
{
try
{
await fetchingAll;
}
catch (AggregateException ex)
{
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
}
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
});
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
@ -230,6 +248,7 @@ namespace BTCPayServer.Controllers
{
try
{
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
@ -242,8 +261,13 @@ namespace BTCPayServer.Controllers
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
paymentMethod.PreferOnion = this.Request.IsOnion();
using (logs.Measure($"{logPrefix} Payment method details creation"))
{
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
}
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
@ -271,7 +295,7 @@ namespace BTCPayServer.Controllers
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
logs.Write($"{logPrefix} {errorMessage}");
return null;
}
}

View File

@ -113,6 +113,7 @@ namespace BTCPayServer.Controllers
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
await _userManager.SetUserNameAsync(user, model.Username);
}
var phoneNumber = user.PhoneNumber;

View File

@ -182,7 +182,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
Action = "Delete"
});
}
@ -229,12 +229,12 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default)
{
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
if (result.AmountDue <= 0)
{
if (redirectToInvoice)

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
network);
return View(new ShowLightningNodeInfoViewModel()

View File

@ -91,7 +91,6 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -140,15 +139,10 @@ namespace BTCPayServer.Controllers
if (currencyPairs == null)
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
result.StatusCode = 400;
return result;
}

View File

@ -27,6 +27,9 @@ using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using System.Runtime.CompilerServices;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data;
namespace BTCPayServer.Controllers
{
@ -39,16 +42,20 @@ namespace BTCPayServer.Controllers
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
private readonly TorServices _torServices;
BTCPayServerOptions _Options;
ApplicationDbContextFactory _ContextFactory;
public ServerController(UserManager<ApplicationUser> userManager,
Configuration.BTCPayServerOptions options,
BTCPayServerOptions options,
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
IHttpClientFactory httpClientFactory,
LightningConfigurationProvider lnConfigProvider,
Services.Stores.StoreRepository storeRepository)
TorServices torServices,
StoreRepository storeRepository,
ApplicationDbContextFactory contextFactory)
{
_Options = options;
_UserManager = userManager;
@ -58,6 +65,8 @@ namespace BTCPayServer.Controllers
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
_torServices = torServices;
_ContextFactory = contextFactory;
}
[Route("server/rates")]
@ -246,6 +255,13 @@ namespace BTCPayServer.Controllers
return error;
StatusMessage = $"The server might restart soon if an update is available...";
}
else if (command == "clean")
{
var error = RunSSH(vm, $"btcpay-clean.sh");
if (error != null)
return error;
StatusMessage = $"The old docker images will be cleaned soon...";
}
else
{
return NotFound();
@ -431,15 +447,46 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Policies()
{
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
selectList.Insert(0, new SelectListItem("(None)", null));
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
}
return View(data);
}
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
if (!String.IsNullOrEmpty(settings.RootAppId))
{
using (var ctx = _ContextFactory.CreateContext())
{
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
if (app != null)
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
else
settings.RootAppType = null;
}
}
else
{
// not preserved on client side, but clearing it just in case
settings.RootAppType = null;
}
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies updated successfully";
return View(settings);
return RedirectToAction(nameof(Policies));
}
[Route("server/services")]
@ -463,6 +510,25 @@ namespace BTCPayServer.Controllers
Link = this.Url.Action(nameof(SSHService))
});
}
foreach (var torService in _torServices.Services)
{
if (torService.VirtualPort == 80)
{
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"http://{torService.OnionHost}"
});
}
else
{
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
});
}
}
return View(result);
}
@ -651,7 +717,7 @@ namespace BTCPayServer.Controllers
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
}
var server = IsLocalNetwork(settings.Server) ? this.Request.Host.Host: settings.Server;
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
SSHServiceViewModel vm = new SSHServiceViewModel();
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
@ -661,13 +727,6 @@ namespace BTCPayServer.Controllers
return View(vm);
}
private static bool IsLocalNetwork(string server)
{
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{

View File

@ -28,6 +28,7 @@ namespace BTCPayServer.Controllers
vm.MerchantId = existing.MerchantId;
vm.Enabled = existing.Enabled;
vm.Mode = existing.Mode;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
}
[HttpPost]
@ -50,7 +51,8 @@ namespace BTCPayServer.Controllers
{
MerchantId = vm.MerchantId,
Enabled = vm.Enabled,
Mode = vm.Mode
Mode = vm.Mode,
AmountMarkupPercentage = vm.AmountMarkupPercentage
};
switch (command)

View File

@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.GetNodeInfo(paymentMethod, network);
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))

View File

@ -189,24 +189,39 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{storeId}/rates")]
public IActionResult Rates()
public IActionResult Rates(string storeId)
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = storeId;
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
return View(vm);
}
[HttpPost]
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, CancellationToken cancellationToken = default)
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null;
try
{
currencyPairs = model.DefaultCurrencyPairs?
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => CurrencyPair.Parse(p))
.ToArray();
}
catch
{
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
}
if (!ModelState.IsValid)
{
return View(model);
@ -220,7 +235,7 @@ namespace BTCPayServer.Controllers
blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting)
{
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
@ -333,13 +348,15 @@ namespace BTCPayServer.Controllers
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
SetCryptoCurrencies(vm, StoreData);
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
@ -396,13 +413,15 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.OnChainMinValue = onchainMinValue;
blob.LightningMaxValue = lightningMaxValue;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.RedirectAutomatically = model.RedirectAutomatically;
if (StoreData.SetStoreBlob(blob))
{
needUpdate = true;
@ -490,7 +509,7 @@ namespace BTCPayServer.Controllers
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
@ -594,7 +613,6 @@ namespace BTCPayServer.Controllers
model.StoreNotConfigured = StoreNotConfigured;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Facade,
Label = t.Label,
SIN = t.SIN,
Id = t.Value
@ -679,7 +697,6 @@ namespace BTCPayServer.Controllers
var tokenRequest = new TokenRequest()
{
Facade = model.Facade,
Label = model.Label,
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
};
@ -691,7 +708,6 @@ namespace BTCPayServer.Controllers
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = tokenRequest.PairingCode,
Facade = model.Facade,
Label = model.Label,
});
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
@ -731,7 +747,6 @@ namespace BTCPayServer.Controllers
}
}
var model = new CreateTokenViewModel();
model.Facade = "merchant";
ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null;
@ -783,7 +798,6 @@ namespace BTCPayServer.Controllers
return View(new PairingModel()
{
Id = pairing.Id,
Facade = pairing.Facade,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
@ -874,7 +888,11 @@ namespace BTCPayServer.Controllers
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
StoreId = store.Id
StoreId = store.Id,
ButtonType = 0,
Min = 1,
Max = 20,
Step = 1
};
return View(model);
}

View File

@ -12,11 +12,6 @@ namespace BTCPayServer.Data
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;

View File

@ -11,7 +11,7 @@ namespace BTCPayServer.Data
{
get; set;
}
[Obsolete("Unused")]
public string Facade
{
get; set;

View File

@ -307,6 +307,25 @@ namespace BTCPayServer.Data
public bool RequiresRefundEmail { get; set; }
CurrencyPair[] _DefaultCurrencyPairs;
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
public CurrencyPair[] DefaultCurrencyPairs
{
get
{
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
}
set
{
_DefaultCurrencyPairs = value;
}
}
public string GetDefaultCurrencyPairString()
{
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
}
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
@ -330,10 +349,11 @@ namespace BTCPayServer.Data
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue OnChainMinValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
public bool LightningAmountInSatoshi { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomLogo { get; set; }
@ -429,6 +449,7 @@ namespace BTCPayServer.Data
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public bool RedirectAutomatically { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{

View File

@ -33,6 +33,7 @@ using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
using System.Net;
namespace BTCPayServer
{
@ -165,6 +166,31 @@ namespace BTCPayServer
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
}
public static bool IsLocalNetwork(string server)
{
if (server == null)
throw new ArgumentNullException(nameof(server));
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
{
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
}
if(IPAddress.TryParse(server, out var ip))
{
return ip.IsLocal();
}
return false;
}
public static bool IsOnion(this HttpRequest request)
{
if (request?.Host.Host == null)
return false;
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
@ -303,30 +329,6 @@ namespace BTCPayServer
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
}
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
return await doing;
}
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
}
}
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
{
ctx.Items.TryGetValue("BitpayAuth", out object obj);

View File

@ -1,4 +1,5 @@
using System;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -34,6 +35,8 @@ namespace BTCPayServer.HostedServices
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_Stop == null)
return;
_Stop.Cancel();
try
{
@ -43,7 +46,14 @@ namespace BTCPayServer.HostedServices
{
}
await BackgroundJobClient.WaitAllRunning(cancellationToken);
try
{
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
catch (OperationCanceledException)
{
}
}
}
@ -51,10 +61,10 @@ namespace BTCPayServer.HostedServices
{
class BackgroundJob
{
public Func<Task> Action;
public Func<CancellationToken, Task> Action;
public TimeSpan Delay;
public IDelay DelayImplementation;
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
public BackgroundJob(Func<CancellationToken, Task> action, TimeSpan delay, IDelay delayImplementation)
{
this.Action = action;
this.Delay = delay;
@ -64,7 +74,7 @@ namespace BTCPayServer.HostedServices
public async Task Run(CancellationToken cancellationToken)
{
await DelayImplementation.Wait(Delay, cancellationToken);
await Action();
await Action(cancellationToken);
}
}
@ -79,9 +89,9 @@ namespace BTCPayServer.HostedServices
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
HashSet<Task> _Processing = new HashSet<Task>();
public void Schedule(Func<Task> action, TimeSpan delay)
public void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn)
{
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
_Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay));
}
public async Task WaitAllRunning(CancellationToken cancellationToken)
@ -89,6 +99,8 @@ namespace BTCPayServer.HostedServices
Task[] processing = null;
lock (_Processing)
{
if (_Processing.Count == 0)
return;
processing = _Processing.ToArray();
}
@ -96,9 +108,8 @@ namespace BTCPayServer.HostedServices
{
await Task.WhenAll(processing).WithCancellation(cancellationToken);
}
catch (Exception) when (cancellationToken.IsCancellationRequested)
catch (Exception) when (!cancellationToken.IsCancellationRequested)
{
throw;
}
}
@ -113,8 +124,7 @@ namespace BTCPayServer.HostedServices
{
_Processing.Add(processing);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
processing.ContinueWith(t =>
_ = processing.ContinueWith(t =>
{
if (t.IsFaulted)
{
@ -125,7 +135,6 @@ namespace BTCPayServer.HostedServices
_Processing.Remove(processing);
}
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
}

View File

@ -59,6 +59,8 @@ namespace BTCPayServer.HostedServices
public Task StopAsync(CancellationToken cancellationToken)
{
if (_Cts == null)
return Task.CompletedTask;
_Cts.Cancel();
return Task.WhenAll(_Tasks);
}

View File

@ -13,6 +13,7 @@ using BTCPayServer.Events;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Filters;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.HostedServices
{
@ -44,13 +45,20 @@ namespace BTCPayServer.HostedServices
get { return _creativeStartUri; }
}
public bool ShowRegister { get; set; }
public bool DiscourageSearchEngines { get; set; }
public AppType? RootAppType { get; set; }
public string RootAppId { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
DiscourageSearchEngines = data.DiscourageSearchEngines;
RootAppType = data.RootAppType;
RootAppId = data.RootAppId;
}
}

View File

@ -24,7 +24,7 @@ namespace BTCPayServer.HostedServices
{
public class InvoiceNotificationManager : IHostedService
{
public static HttpClient _Client = new HttpClient();
HttpClient _Client;
public class ScheduledJob
{
@ -46,6 +46,7 @@ namespace BTCPayServer.HostedServices
private readonly EmailSenderFactory _EmailSenderFactory;
public InvoiceNotificationManager(
IHttpClientFactory httpClientFactory,
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
@ -53,6 +54,7 @@ namespace BTCPayServer.HostedServices
ILogger<InvoiceNotificationManager> logger,
EmailSenderFactory emailSenderFactory)
{
_Client = httpClientFactory.CreateClient();
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
@ -135,23 +137,29 @@ namespace BTCPayServer.HostedServices
return;
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceStr, cancellation), TimeSpan.Zero);
}
public async Task NotifyHttp(string invoiceData)
public async Task NotifyHttp(string invoiceData, CancellationToken cancellationToken)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
bool reschedule = false;
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
reschedule = !response.IsSuccessStatusCode;
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// When the JobClient will be persistent, this will reschedule the job for after reboot
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
return;
}
catch (OperationCanceledException)
{
aggregatorEvent.Error = "Timeout";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
@ -172,14 +180,13 @@ namespace BTCPayServer.HostedServices
aggregatorEvent.Error = $"Unexpected error: {message}";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
finally { cts?.Dispose(); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
}
}
@ -203,7 +210,7 @@ namespace BTCPayServer.HostedServices
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellationToken)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
@ -224,7 +231,14 @@ namespace BTCPayServer.HostedServices
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
var response = await Enqueue(notification.Data.Id, async () =>
{
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
cts.CancelAfter(TimeSpan.FromMinutes(1.0));
return await _Client.SendAsync(request, cts.Token);
}
});
return response;
}

View File

@ -49,7 +49,6 @@ namespace BTCPayServer.HostedServices
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
@ -99,8 +98,8 @@ namespace BTCPayServer.HostedServices
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
}
}
@ -185,19 +184,6 @@ namespace BTCPayServer.HostedServices
return result;
}
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
}
}
private void Watch(string invoiceId)
{
if (invoiceId == null)
@ -231,25 +217,24 @@ namespace BTCPayServer.HostedServices
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
Task _Loop;
Task _WaitingInvoices;
CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Loop = StartLoop(_Cts.Token);
_WaitingInvoices = WaitPendingInvoices();
_ = WaitPendingInvoices();
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
{
Watch(b.InvoiceId);
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(b =>
{
if (b.Name == InvoiceEvent.Created)
{
Watch(b.Invoice.Id);
await Wait(b.Invoice.Id);
_ = Wait(b.Invoice.Id);
}
if (b.Name == InvoiceEvent.ReceivedPayment)
@ -264,79 +249,76 @@ namespace BTCPayServer.HostedServices
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(id => Wait(id)).ToArray());
_WaitingInvoices = null;
}
async Task StartLoop(CancellationToken cancellation)
{
Logs.PayServer.LogInformation("Start watching invoices");
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
try
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
{
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
int maxLoop = 5;
int loopCount = -1;
while (loopCount < maxLoop)
{
int maxLoop = 5;
int loopCount = -1;
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
loopCount++;
try
{
loopCount++;
try
cancellation.ThrowIfCancellationRequested();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == InvoiceStatus.Complete ||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
break;
}
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == InvoiceStatus.Complete ||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Delay(10000, cancellation)
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
if (updateContext.Events.Count == 0)
break;
}
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
_ = Task.Delay(10000, cancellation)
.ContinueWith(t => Watch(invoiceId), TaskScheduler.Default);
break;
}
}
}
catch when (cancellation.IsCancellationRequested)
{
}
Logs.PayServer.LogInformation("Stop watching invoices");
}
public Task StopAsync(CancellationToken cancellationToken)
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_Cts == null)
return;
leases.Dispose();
_Cts.Cancel();
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
return Task.WhenAll(waitingPendingInvoices, _Loop);
try
{
await _Loop;
}
catch { }
finally
{
Logs.PayServer.LogInformation("Stop watching invoices");
}
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -68,7 +69,7 @@ namespace BTCPayServer.HostedServices
var exchanges = new CoinAverageExchanges();
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
{
exchanges.Add(item);
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.HostedServices
{
public class TorServicesHostedService : BaseAsyncService
{
private readonly BTCPayServerOptions _options;
private readonly TorServices _torServices;
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
{
_options = options;
_torServices = torServices;
}
internal override Task[] InitializeTasks()
{
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
if (string.IsNullOrEmpty(_options.TorrcFile))
return Array.Empty<Task>();
return new Task[] { CreateLoopTask(RefreshTorServices) };
}
async Task RefreshTorServices()
{
await _torServices.Refresh();
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
}
}
}

View File

@ -36,7 +36,6 @@ using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments.Changelly;
@ -48,6 +47,7 @@ using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
using BundlerMinifier.TagHelpers;
namespace BTCPayServer.Hosting
{
@ -62,6 +62,9 @@ namespace BTCPayServer.Hosting
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<TorServices>();
services.TryAddSingleton<SocketFactory>();
services.TryAddSingleton<LightningClientFactoryService>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<InvoiceRepository>(o =>
@ -188,6 +191,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
@ -215,7 +219,7 @@ namespace BTCPayServer.Hosting
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddBundles();
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
services.AddTransient<BundleOptions>(provider =>
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();

View File

@ -88,6 +88,9 @@ namespace BTCPayServer.Hosting
if (!httpContext.Request.Path.HasValue)
return false;
// In case of anyone can create invoice, the storeId can be set explicitely
bitpayAuth |= httpContext.Request.Query.ContainsKey("storeid");
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
var method = httpContext.Request.Method;
@ -95,7 +98,7 @@ namespace BTCPayServer.Hosting
if (
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
(path == "/invoices" || path == "/invoices/") &&
(isCors || (method == "POST" && isJson)))
return true;

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Hosting
{
public class ResourceBundleProvider : IBundleProvider
{
BundleProvider _InnerProvider;
Lazy<Dictionary<string, Bundle>> _BundlesByName;
public ResourceBundleProvider(IHostingEnvironment hosting, BundleOptions options)
{
if (options.UseBundles)
{
_BundlesByName = new Lazy<Dictionary<string, Bundle>>(() =>
{
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json"))
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
var content = reader.ReadToEnd();
return JArray.Parse(content).OfType<JObject>()
.Select(jobj => new Bundle()
{
Name = jobj.Property("name")?.Value.Value<string>() ?? jobj.Property("outputFileName").Value.Value<string>(),
OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName").Value.Value<string>())
}).ToDictionary(o => o.Name, o => o);
}
}, true);
}
else
{
_InnerProvider = new BundleProvider();
}
}
public Bundle GetBundle(string name)
{
if (_InnerProvider != null)
return _InnerProvider.GetBundle(name);
_BundlesByName.Value.TryGetValue(name, out var bundle);
return bundle;
}
}
}

View File

@ -34,7 +34,6 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.PaymentRequest;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
@ -65,6 +64,7 @@ namespace BTCPayServer.Hosting
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddSession();
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
@ -79,7 +79,7 @@ namespace BTCPayServer.Hosting
// StyleSrc = "'self' 'unsafe-inline'",
// ScriptSrc = "'self' 'unsafe-inline'"
//});
});
}).AddControllersAsServices();
services.TryAddScoped<ContentSecurityPolicies>();
services.Configure<IdentityOptions>(options =>
{
@ -170,6 +170,7 @@ namespace BTCPayServer.Hosting
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseSession();
app.UseSignalR(route =>
{
AppHub.Register(route);

View File

@ -0,0 +1,39 @@
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
using BTCPayServer.Rating;
namespace BTCPayServer.JsonConverters
{
public class CurrencyPairJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
CurrencyPair.TryParse((string)reader.Value, out var result) ? result :
throw new JsonObjectException("Invalid currency pair", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid currency pair", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -33,5 +33,35 @@ namespace BTCPayServer.Logging
return _InvoiceLogs.ToList();
}
}
internal IDisposable Measure(string logs)
{
return new Mesuring(this, logs);
}
class Mesuring : IDisposable
{
private readonly InvoiceLogs _logs;
private readonly string _msg;
private readonly DateTimeOffset _Before;
public Mesuring(InvoiceLogs logs, string msg)
{
_logs = logs;
_msg = msg;
_Before = DateTimeOffset.UtcNow;
}
public void Dispose()
{
var timespan = DateTimeOffset.UtcNow - _Before;
if (timespan.TotalSeconds >= 1.0)
{
_logs.Write($"{_msg} took {(int)timespan.TotalSeconds} seconds");
}
else
{
_logs.Write($"{_msg} took {(int)timespan.TotalMilliseconds} milliseconds");
}
}
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.AppViewModels
{
@ -16,8 +17,13 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Featured Image")]
public string MainImageUrl { get; set; }
[Display(Name = "Callback Notification Url")]
[Uri]
public string NotificationUrl { get; set; }
[Display(Name = "Invoice Email Notification")]
[EmailAddress]
public string NotificationEmail { get; set; }
[Required]
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]

View File

@ -1,4 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.AppViewModels
{
@ -10,7 +13,6 @@ namespace BTCPayServer.Models.AppViewModels
[Required]
[MaxLength(5)]
public string Currency { get; set; }
[MaxLength(5000)]
public string Template { get; set; }
[Display(Name = "Enable shopping cart")]
@ -25,6 +27,13 @@ namespace BTCPayServer.Models.AppViewModels
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
public string InvoiceUrl { get; internal set; }
[Display(Name = "Callback Notification Url")]
[Uri]
public string NotificationUrl { get; set; }
[Display(Name = "Invoice Email Notification")]
[EmailAddress]
public string NotificationEmail { get; set; }
[Required]
[MaxLength(30)]
@ -47,5 +56,28 @@ namespace BTCPayServer.Models.AppViewModels
public string CustomCSSLink { get; set; }
public string Id { get; set; }
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
public string RedirectAutomatically { get; set; } = string.Empty;
public SelectList RedirectAutomaticallySelectList =>
new SelectList(new List< SelectListItem>()
{
new SelectListItem()
{
Text = "Yes",
Value = "true"
},
new SelectListItem()
{
Text = "No",
Value = "false"
},
new SelectListItem()
{
Text = "Use Store Settings",
Value = ""
}
}, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically);
}
}

View File

@ -75,6 +75,7 @@ namespace BTCPayServer.Models.AppViewModels
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
public bool DisplayPerksRanking { get; set; }
public bool Enabled { get; set; }
public string ResetEvery { get; set; }
}
public class ContributeToCrowdfund

View File

@ -79,5 +79,8 @@ namespace BTCPayServer.Models
public string Guid { get; set; }
[JsonProperty(PropertyName = "token", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Token { get; set; }
[JsonProperty(PropertyName = "redirectAutomatically", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? RedirectAutomatically { get; set; }
}
}

View File

@ -39,7 +39,7 @@ namespace BTCPayServer.Models
{
JObject item = new JObject();
jarray.Add(item);
JProperty jProp = new JProperty(token.Facade);
JProperty jProp = new JProperty("merchant");
item.Add(jProp);
jProp.Value = token.Value;
}

View File

@ -20,6 +20,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string CustomCSSLink { get; set; }
public string CustomLogoLink { get; set; }
public string DefaultLang { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
@ -65,5 +66,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string CoinSwitchMode { get; set; }
public string CoinSwitchMerchantId { get; set; }
public string RootPath { get; set; }
public decimal CoinSwitchAmountMarkupPercentage { get; set; }
public bool RedirectAutomatically { get; set; }
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
namespace BTCPayServer.Models.ServerViewModels
{
@ -17,5 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> TorHttpServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> TorOtherServices { get; set; } = new List<OtherExternalService>();
}
}

View File

@ -21,23 +21,10 @@ namespace BTCPayServer.Models.StoreViewModels
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default the default payment method on checkout")]
[Display(Name = "Default payment method on checkout")]
public string DefaultPaymentMethod { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[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; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Uri]
@ -49,9 +36,26 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { 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; }
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Display lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
public bool RedirectAutomatically { get; set; }
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);

View File

@ -27,10 +27,6 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
public string Facade
{
get; set;
}
public string SIN
{
get; set;

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.ModelBinders;
using Microsoft.AspNetCore.Mvc;
@ -18,6 +15,13 @@ namespace BTCPayServer.Models.StoreViewModels
public string CheckoutDesc { get; set; }
public string OrderId { get; set; }
public int ButtonSize { get; set; }
public int ButtonType { get; set; }
// Slider properties (ButtonType = 2)
public decimal Min { get; set; }
public decimal Max { get; set; }
public decimal Step { get; set; }
[Url]
public string ServerIpn { get; set; }
[Url]

View File

@ -21,14 +21,16 @@ namespace BTCPayServer.Models.StoreViewModels
{
public string Name { get; set; }
public string Value { get; set; }
public string Url { get; set; }
}
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
{
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).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;
RateSource = chosen.Url;
}
public List<TestResultViewModel> TestRateRules { get; set; }
@ -42,6 +44,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string Script { get; set; }
public string DefaultScript { get; set; }
public string ScriptTest { get; set; }
public string DefaultCurrencyPairs { get; set; }
public string StoreId { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Add a spread on exchange rate of ... %")]
@ -57,10 +61,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string RateSource
{
get
{
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
get;
set;
}
}
}

View File

@ -21,12 +21,6 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Required]
public string Facade
{
get; set;
}
[Required]
public string StoreId
{
@ -52,10 +46,6 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
public string Facade
{
get; set;
}
}
public class TokensViewModel
{

View File

@ -12,6 +12,12 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Integration Mode")]
public string Mode { get; set; } = "inline";
[Required]
[Range(0, 100)]
[Display(Name =
"Percentage to multiply amount requested at Coinswitch to avoid underpaid situations due to Coinswitch not guaranteeing rates. ")]
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
public List<SelectListItem> Modes { get; } = new List<SelectListItem>
{

View File

@ -20,11 +20,6 @@ namespace BTCPayServer.Models
{
get; set;
}
[JsonProperty(PropertyName = "facade")]
public string Facade
{
get; set;
}
[JsonProperty(PropertyName = "count")]
public int Count
{

View File

@ -5,6 +5,7 @@ namespace BTCPayServer.Payments.CoinSwitch
public string MerchantId { get; set; }
public string Mode { get; set; }
public bool Enabled { get; set; }
public decimal AmountMarkupPercentage { get; set; }
public bool IsConfigured()
{

View File

@ -1,14 +1,16 @@
using System;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning
{
@ -17,18 +19,25 @@ namespace BTCPayServer.Payments.Lightning
public static int LIGHTNING_TIMEOUT = 5000;
NBXplorerDashboard _Dashboard;
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly SocketFactory _socketFactory;
public LightningLikePaymentHandler(
NBXplorerDashboard dashboard)
NBXplorerDashboard dashboard,
LightningClientFactoryService lightningClientFactory,
SocketFactory socketFactory)
{
_Dashboard = dashboard;
_lightningClientFactory = lightningClientFactory;
_socketFactory = socketFactory;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(supportedPaymentMethod, network);
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = supportedPaymentMethod.CreateClient(network);
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1);
@ -63,14 +72,14 @@ namespace BTCPayServer.Payments.Lightning
};
}
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
public async Task<NodeInfo> GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
var client = supportedPaymentMethod.CreateClient(network);
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
LightningNodeInformation info = null;
try
{
@ -84,8 +93,8 @@ namespace BTCPayServer.Payments.Lightning
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
if (info.NodeInfo == null)
var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault();
if (nodeInfo == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
@ -96,7 +105,7 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
}
return info.NodeInfo;
return nodeInfo;
}
}
@ -104,22 +113,11 @@ namespace BTCPayServer.Payments.Lightning
{
try
{
IPAddress address = null;
try
{
address = IPAddress.Parse(nodeInfo.Host);
}
catch
{
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
}
if (!Utils.TryParseEndpoint(nodeInfo.Host, nodeInfo.Port, out var endpoint))
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation))
{
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
}
}
catch (Exception ex)

View File

@ -10,117 +10,150 @@ using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using BTCPayServer.Lightning;
using System.Collections.Concurrent;
using System.Threading.Channels;
using Microsoft.Extensions.Caching.Memory;
using System.Net.Http;
using BTCPayServer.Services;
namespace BTCPayServer.Payments.Lightning
{
public class LightningListener : IHostedService
{
class ListenedInvoice
{
public LightningLikePaymentMethodDetails PaymentMethodDetails { get; set; }
public LightningSupportedPaymentMethod SupportedPaymentMethod { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string Uri { get; internal set; }
public BTCPayNetwork Network { get; internal set; }
public string InvoiceId { get; internal set; }
}
EventAggregator _Aggregator;
InvoiceRepository _InvoiceRepository;
private readonly IMemoryCache _memoryCache;
BTCPayNetworkProvider _NetworkProvider;
private readonly LightningClientFactoryService lightningClientFactory;
Channel<string> _CheckInvoices = Channel.CreateUnbounded<string>();
Task _CheckingInvoice;
Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>();
public LightningListener(EventAggregator aggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider)
IMemoryCache memoryCache,
BTCPayNetworkProvider networkProvider,
LightningClientFactoryService lightningClientFactory,
IHttpClientFactory httpClientFactory)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
_memoryCache = memoryCache;
_NetworkProvider = networkProvider;
this.lightningClientFactory = lightningClientFactory;
}
async Task CheckingInvoice(CancellationToken cancellation)
{
while(await _CheckInvoices.Reader.WaitToReadAsync(cancellation) &&
_CheckInvoices.Reader.TryRead(out var invoiceId))
{
try
{
foreach (var listenedInvoice in (await GetListenedInvoices(invoiceId)).Where(i => !i.IsExpired()))
{
var instanceListenerKey = (listenedInvoice.Network.CryptoCode, listenedInvoice.SupportedPaymentMethod.GetLightningUrl().ToString());
if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener) ||
!instanceListener.IsListening)
{
instanceListener = instanceListener ?? new LightningInstanceListener(_InvoiceRepository, _Aggregator, listenedInvoice.SupportedPaymentMethod, lightningClientFactory, listenedInvoice.Network);
var status = await instanceListener.PollPayment(listenedInvoice, cancellation);
if (status is null ||
status is LightningInvoiceStatus.Paid ||
status is LightningInvoiceStatus.Expired)
{
continue;
}
instanceListener.AddListenedInvoice(listenedInvoice);
instanceListener.EnsureListening(cancellation);
_InstanceListeners.TryAdd(instanceListenerKey, instanceListener);
}
else
{
instanceListener.AddListenedInvoice(listenedInvoice);
}
}
foreach (var kv in _InstanceListeners)
{
kv.Value.RemoveExpiredInvoices();
}
foreach (var k in _InstanceListeners
.Where(kv => !kv.Value.IsListening)
.Select(kv => kv.Key).ToArray())
{
_InstanceListeners.Remove(k);
}
}
catch when (!_Cts.Token.IsCancellationRequested)
{
}
}
}
private Task<List<ListenedInvoice>> GetListenedInvoices(string invoiceId)
{
return _memoryCache.GetOrCreateAsync(invoiceId, async (cacheEntry) =>
{
var listenedInvoices = new List<ListenedInvoice>();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
{
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
if (lightningMethod == null)
continue;
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
if (lightningSupportedMethod == null)
continue;
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
listenedInvoices.Add(new ListenedInvoice()
{
Expiration = invoice.ExpirationTime,
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
Network = network,
InvoiceId = invoice.Id
});
}
var expiredIn = DateTimeOffset.UtcNow - invoice.ExpirationTime;
cacheEntry.AbsoluteExpiration = DateTimeOffset.UtcNow + (expiredIn >= TimeSpan.FromMinutes(5.0) ? expiredIn : TimeSpan.FromMinutes(5.0));
return listenedInvoices;
});
}
ConcurrentDictionary<string, LightningInstanceListener> _ListeningInstances = new ConcurrentDictionary<string, LightningInstanceListener>();
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(inv =>
{
if (inv.Name == InvoiceEvent.Created)
{
await EnsureListening(inv.Invoice.Id, false);
_CheckInvoices.Writer.TryWrite(inv.Invoice.Id);
}
}));
_CheckingInvoice = CheckingInvoice(_Cts.Token);
_ListenPoller = new Timer(async s =>
{
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");
var invoiceIds = await _InvoiceRepository.GetPendingInvoices();
foreach (var invoiceId in invoiceIds)
_CheckInvoices.Writer.TryWrite(invoiceId);
}
catch { } // Never throw an unhandled exception on async void
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
return Task.CompletedTask;
}
private async Task EnsureListening(string invoiceId, bool poll)
{
if (Listening(invoiceId))
return;
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
{
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
if (lightningMethod == null)
continue;
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
if (lightningSupportedMethod == null)
continue;
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
var listenedInvoice = new ListenedInvoice()
{
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
Network = network,
InvoiceId = invoice.Id
};
if (poll)
{
var charge = lightningSupportedMethod.CreateClient(network);
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 == LightningInvoiceStatus.Paid)
await AddPayment(network, chargeInvoice, listenedInvoice);
if (chargeInvoice.Status == LightningInvoiceStatus.Paid || chargeInvoice.Status == LightningInvoiceStatus.Expired)
continue;
}
StartListening(listenedInvoice);
}
}
TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0);
public TimeSpan PollInterval
{
@ -139,159 +172,194 @@ namespace BTCPayServer.Payments.Lightning
}
CancellationTokenSource _Cts = new CancellationTokenSource();
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
ILightningInvoiceListener session = null;
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
var lightningClient = supportedPaymentMethod.CreateClient(network);
session = await lightningClient.Listen(_Cts.Token);
while (true)
{
var notification = await session.WaitInvoice(_Cts.Token);
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
if (listenedInvoice == null)
continue;
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == LightningInvoiceStatus.Paid &&
notification.PaidAt.HasValue && notification.Amount != null)
{
await AddPayment(network, notification, listenedInvoice);
if (DoneListening(listenedInvoice))
break;
}
if (notification.Status == LightningInvoiceStatus.Expired)
{
if (DoneListening(listenedInvoice))
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
DoneListening(supportedPaymentMethod.GetLightningUrl());
}
finally
{
session?.Dispose();
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
{
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
Amount = notification.Amount
}, network, accounted: true);
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
if (invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
}
}
List<Task> _ListeningLightning = new List<Task>();
MultiValueDictionary<string, ListenedInvoice> _ListenedInvoiceByLightningUrl = new MultiValueDictionary<string, ListenedInvoice>();
Dictionary<string, ListenedInvoice> _ListenedInvoiceByChargeInvoiceId = new Dictionary<string, ListenedInvoice>();
HashSet<string> _InvoiceIds = new HashSet<string>();
private Timer _ListenPoller;
/// <summary>
/// Stop listening an invoice
/// </summary>
/// <param name="listenedInvoice">The invoice to stop listening</param>
/// <returns>true if still need to listen the lightning instance</returns>
bool DoneListening(ListenedInvoice listenedInvoice)
{
lock (_ListenedInvoiceByLightningUrl)
{
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
_ListenedInvoiceByLightningUrl.Remove(listenedInvoice.Uri, listenedInvoice);
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
{
return true;
}
}
return false;
}
/// <summary>
/// Stop listening all invoices on this server
/// </summary>
/// <param name="uri"></param>
private void DoneListening(LightningConnectionString connectionString)
{
var uri = connectionString.BaseUri;
lock (_ListenedInvoiceByChargeInvoiceId)
{
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
{
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
}
_ListenedInvoiceByLightningUrl.Remove(uri.AbsoluteUri);
}
}
bool Listening(string invoiceId)
{
lock (_ListenedInvoiceByLightningUrl)
{
return _InvoiceIds.Contains(invoiceId);
}
}
private ListenedInvoice GetListenedInvoice(string chargeInvoiceId)
{
ListenedInvoice listenedInvoice = null;
lock (_ListenedInvoiceByLightningUrl)
{
_ListenedInvoiceByChargeInvoiceId.TryGetValue(chargeInvoiceId, out listenedInvoice);
}
return listenedInvoice;
}
bool StartListening(ListenedInvoice listenedInvoice)
{
lock (_ListenedInvoiceByLightningUrl)
{
if (_InvoiceIds.Contains(listenedInvoice.InvoiceId))
return false;
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
{
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
_ListeningLightning.Add(listen);
}
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
_InvoiceIds.Add(listenedInvoice.InvoiceId);
}
return true;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
Task[] listening = null;
lock (_ListenedInvoiceByLightningUrl)
try
{
listening = _ListeningLightning.ToArray();
await _CheckingInvoice;
}
await Task.WhenAll(listening);
catch (OperationCanceledException)
{
}
try
{
await Task.WhenAll(_ListeningInstances.Select(c => c.Value.Listening).ToArray());
}
catch (OperationCanceledException)
{
}
Logs.PayServer.LogInformation("Lightning listened stopped");
}
}
public class LightningInstanceListener
{
private LightningSupportedPaymentMethod supportedPaymentMethod;
private readonly InvoiceRepository invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetwork network;
private readonly LightningClientFactoryService _lightningClientFactory;
public LightningInstanceListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
LightningSupportedPaymentMethod supportedPaymentMethod,
LightningClientFactoryService lightningClientFactory,
BTCPayNetwork network)
{
this.supportedPaymentMethod = supportedPaymentMethod;
this.invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
this.network = network;
_lightningClientFactory = lightningClientFactory;
}
internal bool AddListenedInvoice(ListenedInvoice invoice)
{
return _ListenedInvoices.TryAdd(invoice.PaymentMethodDetails.InvoiceId, invoice);
}
internal async Task<LightningInvoiceStatus?> PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation)
{
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId);
if (lightningInvoice?.Status is LightningInvoiceStatus.Paid &&
await AddPayment(lightningInvoice, listenedInvoice.InvoiceId))
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}");
}
return lightningInvoice?.Status;
}
public bool IsListening => Listening?.Status is TaskStatus.Running || Listening?.Status is TaskStatus.WaitingForActivation;
public Task Listening { get; set; }
public void EnsureListening(CancellationToken cancellation)
{
if (!IsListening)
{
StopListeningCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
Listening = Listen(StopListeningCancellationTokenSource.Token);
}
}
public CancellationTokenSource StopListeningCancellationTokenSource;
async Task Listen(CancellationToken cancellation)
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
try
{
var lightningClient = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
using (var session = await lightningClient.Listen(cancellation))
{
// Just in case the payment arrived after our last poll but before we listened.
await PollAllListenedInvoices(cancellation);
if (_ErrorAlreadyLogged)
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Could reconnect successfully to {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
_ErrorAlreadyLogged = false;
while (!_ListenedInvoices.IsEmpty)
{
var notification = await session.WaitInvoice(cancellation);
if (!_ListenedInvoices.TryGetValue(notification.Id, out var listenedInvoice))
continue;
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == LightningInvoiceStatus.Paid &&
notification.PaidAt.HasValue && notification.Amount != null)
{
if (await AddPayment(notification, listenedInvoice.InvoiceId))
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})");
}
_ListenedInvoices.TryRemove(notification.Id, out var _);
}
else if (notification.Status == LightningInvoiceStatus.Expired)
{
_ListenedInvoices.TryRemove(notification.Id, out var _);
}
}
}
}
}
catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged)
{
_ErrorAlreadyLogged = true;
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
if (_ListenedInvoices.IsEmpty)
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): No more invoice to listen on {supportedPaymentMethod.GetLightningUrl().BaseUri}, releasing the connection.");
}
public DateTimeOffset? LastFullPoll { get; set; }
internal async Task PollAllListenedInvoices(CancellationToken cancellation)
{
foreach (var invoice in _ListenedInvoices.Values)
{
var status = await PollPayment(invoice, cancellation);
if (status is null ||
status is LightningInvoiceStatus.Paid ||
status is LightningInvoiceStatus.Expired)
_ListenedInvoices.TryRemove(invoice.PaymentMethodDetails.InvoiceId, out var _);
}
LastFullPoll = DateTimeOffset.UtcNow;
if (_ListenedInvoices.IsEmpty)
{
StopListeningCancellationTokenSource?.Cancel();
}
}
bool _ErrorAlreadyLogged = false;
ConcurrentDictionary<string, ListenedInvoice> _ListenedInvoices = new ConcurrentDictionary<string, ListenedInvoice>();
public async Task<bool> AddPayment(LightningInvoice notification, string invoiceId)
{
var payment = await invoiceRepository.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable
}, network, accounted: true);
if (payment != null)
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
if (invoice != null)
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
}
return payment != null;
}
internal void RemoveExpiredInvoices()
{
foreach (var invoice in _ListenedInvoices)
{
if (invoice.Value.IsExpired())
_ListenedInvoices.TryRemove(invoice.Key, out var _);
}
if (_ListenedInvoices.IsEmpty)
StopListeningCancellationTokenSource?.Cancel();
}
}
class ListenedInvoice
{
public bool IsExpired() { return DateTimeOffset.UtcNow > Expiration; }
public DateTimeOffset Expiration { get; set; }
public LightningLikePaymentMethodDetails PaymentMethodDetails { get; set; }
public LightningSupportedPaymentMethod SupportedPaymentMethod { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string Uri { get; internal set; }
public BTCPayNetwork Network { get; internal set; }
public string InvoiceId { get; internal set; }
}
}

View File

@ -60,10 +60,5 @@ namespace BTCPayServer.Payments.Lightning
LightningChargeUrl = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
public ILightningClient CreateClient(BTCPayNetwork network)
{
return LightningClientFactory.CreateClient(this.GetLightningUrl(), network.NBitcoinNetwork);
}
}
}

View File

@ -62,9 +62,34 @@ namespace BTCPayServer.Payments
public override string ToString()
{
if (PaymentType == PaymentTypes.BTCLike)
return CryptoCode;
return CryptoCode + "_" + PaymentType.ToString();
return ToString(false);
}
public string ToString(bool pretty)
{
if (pretty)
{
return $"{CryptoCode} ({PrettyMethod(PaymentType)})";
}
else
{
if (PaymentType == PaymentTypes.BTCLike)
return CryptoCode;
return CryptoCode + "_" + PaymentType.ToString();
}
}
private static string PrettyMethod(PaymentTypes paymentType)
{
switch (paymentType)
{
case PaymentTypes.BTCLike:
return "On-Chain";
case PaymentTypes.LightningLike:
return "Off-Chain";
default:
return paymentType.ToString();
}
}
public static bool TryParse(string str, out PaymentMethodId paymentMethodId)

View File

@ -57,38 +57,52 @@ namespace BTCPayServer.Security
List<Claim> claims = new List<Claim>();
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
string storeId = null;
bool anonymous = true;
bool? success = null;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
storeId = result.StoreId;
success = result.SuccessAuth;
anonymous = false;
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
success = storeId != null;
anonymous = false;
}
else
{
if (Context.Request.HttpContext.Request.Query.TryGetValue("storeId", out var storeIdStringValues))
{
storeId = storeIdStringValues.FirstOrDefault() ?? string.Empty;
success = true;
anonymous = true;
}
}
if (success.HasValue)
if (success is true)
{
if (success.Value)
if (storeId != null)
{
if (storeId != null)
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
if (store == null ||
(anonymous && !store.GetStoreBlob().AnyoneCanInvoice))
{
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
return AuthenticateResult.Fail("Invalid credentials");
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
else
{
return AuthenticateResult.Fail("Invalid credentials");
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
else if (success is false)
{
return AuthenticateResult.Fail("Invalid credentials");
}
// else if (success is null)
}
return AuthenticateResult.NoResult();
}
@ -134,7 +148,7 @@ namespace BTCPayServer.Security
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
var bitToken = (await _TokenRepository.GetTokens(sin)).FirstOrDefault();
if (bitToken == null)
{
return (null, false);
@ -170,34 +184,6 @@ namespace BTCPayServer.Security
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if (expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
return null;
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
}
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
{

View File

@ -53,7 +53,11 @@ namespace BTCPayServer.Services.Apps
public async Task<object> GetAppInfo(string appId)
{
var app = await GetApp(appId, AppType.Crowdfund, true);
return await GetInfo(app);
if (app != null)
{
return await GetInfo(app);
}
return null;
}
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage = null)
{
@ -135,6 +139,7 @@ namespace BTCPayServer.Services.Apps
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
DisplayPerksRanking = settings.DisplayPerksRanking,
PerkCount = perkCount,
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,

View File

@ -82,6 +82,8 @@ namespace BTCPayServer.Services.Apps
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav"
};
public string NotificationEmail { get; set; }
}
public enum CrowdfundResetEvery
{

View File

@ -14,7 +14,8 @@ namespace BTCPayServer.Services
public class BTCPayServerEnvironment
{
IHttpContextAccessor httpContext;
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
TorServices torServices;
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext, TorServices torServices)
{
this.httpContext = httpContext;
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
@ -25,6 +26,7 @@ namespace BTCPayServer.Services
#endif
Environment = env;
NetworkType = provider.NetworkType;
this.torServices = torServices;
}
public IHostingEnvironment Environment
{
@ -34,6 +36,8 @@ namespace BTCPayServer.Services
public string ExpectedDomain => httpContext.HttpContext.Request.Host.Host;
public string ExpectedHost => httpContext.HttpContext.Request.Host.Value;
public string ExpectedProtocol => httpContext.HttpContext.Request.Scheme;
public string OnionUrl => this.torServices.Services.Where(s => s.ServiceType == TorServiceType.BTCPayServer)
.Select(s => $"http://{s.OnionHost}").FirstOrDefault();
public NetworkType NetworkType { get; set; }
public string Version
@ -52,6 +56,18 @@ namespace BTCPayServer.Services
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
}
}
public bool IsSecure
{
get
{
return NetworkType != NetworkType.Mainnet ||
httpContext.HttpContext.Request.Scheme == "https" ||
httpContext.HttpContext.Request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) ||
Extensions.IsLocalNetwork(httpContext.HttpContext.Request.Host.Host);
}
}
public override string ToString()
{
StringBuilder txt = new StringBuilder();

View File

@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IBackgroundJobClient
{
void Schedule(Func<Task> act, TimeSpan zero);
void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn);
}
}

View File

@ -298,6 +298,12 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public bool RedirectAutomatically
{
get;
set;
}
[Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")]
public Money TxFee
@ -356,7 +362,6 @@ namespace BTCPayServer.Services.Invoices
return DateTimeOffset.UtcNow > ExpirationTime;
}
public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider)
{
ServerUrl = ServerUrl ?? "";
@ -417,6 +422,22 @@ namespace BTCPayServer.Services.Invoices
var scheme = info.Network.UriScheme;
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
cryptoInfo.Payments = GetPayments(info.Network).Select(entity =>
{
var data = entity.GetCryptoPaymentData();
return new InvoicePaymentInfo()
{
Id = data.GetPaymentId(),
Fee = entity.NetworkFee,
Value = data.GetValue(),
Completed = data.PaymentCompleted(entity, info.Network),
Confirmed = data.PaymentConfirmed(entity, SpeedPolicy, info.Network),
Destination = data.GetDestination(info.Network),
PaymentType = data.GetPaymentType().ToString(),
ReceivedDate = entity.ReceivedTime.DateTime
};
}).ToList();
if (paymentId.PaymentType == PaymentTypes.BTCLike)
{
var minerInfo = new MinerFeeInfo();
@ -717,6 +738,12 @@ namespace BTCPayServer.Services.Invoices
[Obsolete("Use GetId().PaymentType instead")]
public string PaymentType { get; set; }
/// <summary>
/// We only use this to pass a singleton asking to the payment handler to prefer payments through TOR, we don't really
/// need to save this information
/// </summary>
[JsonIgnore]
public bool PreferOnion { get; set; }
public PaymentMethodId GetId()
{

View File

@ -71,14 +71,14 @@ retry:
using (var db = _ContextFactory.CreateContext())
{
var key = scriptPubKey.Hash.ToString() + "#" + cryptoCode;
var result = await db.AddressInvoices
var result = (await db.AddressInvoices
#pragma warning disable CS0618
.Where(a => a.Address == key)
#pragma warning restore CS0618
.Select(a => a.InvoiceData)
.Include(a => a.Payments)
.Include(a => a.RefundAddresses)
.FirstOrDefaultAsync();
.ToListAsync()).FirstOrDefault();
if (result == null)
return null;
return ToEntity(result);
@ -118,7 +118,7 @@ retry:
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, InvoiceLogs creationLogs, BTCPayNetworkProvider networkProvider)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice, null);
@ -165,17 +165,6 @@ retry:
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
foreach (var log in creationLogs.ToList())
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoice.Id,
Message = log.Log,
Timestamp = log.Timestamp,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
@ -191,6 +180,24 @@ retry:
return invoice;
}
public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs)
{
using (var context = _ContextFactory.CreateContext())
{
foreach (var log in logs.ToList())
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoiceId,
Message = log.Log,
Timestamp = log.Timestamp,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
private static string GetDestination(PaymentMethod paymentMethod, Network network)
{
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
@ -206,7 +213,7 @@ retry:
{
using (var context = _ContextFactory.CreateContext())
{
var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId);
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
if (invoice == null)
return false;
@ -376,7 +383,7 @@ retry:
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
query = query.Where(i => i.Id == id);
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
var invoice = (await query.ToListAsync()).FirstOrDefault();
if (invoice == null)
return null;
@ -487,7 +494,7 @@ retry:
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
query = query.Where(i => unused == (i.Status == "invalid" || !string.IsNullOrEmpty(i.ExceptionStatus)));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Services
{
public class LightningClientFactoryService
{
private readonly IHttpClientFactory _httpClientFactory;
public LightningClientFactoryService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public ILightningClient Create(LightningConnectionString lightningConnectionString, BTCPayNetwork network)
{
if (lightningConnectionString == null)
throw new ArgumentNullException(nameof(lightningConnectionString));
if (network == null)
throw new ArgumentNullException(nameof(network));
return new Lightning.LightningClientFactory(network.NBitcoinNetwork)
{
HttpClient = _httpClientFactory.CreateClient($"{network.CryptoCode}: Lightning client")
}.Create(lightningConnectionString);
}
}
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Logging;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Mail;
@ -17,7 +18,7 @@ namespace BTCPayServer.Services.Mails
public void SendEmail(string email, string subject, string message)
{
_JobClient.Schedule(async () =>
_JobClient.Schedule(async (cancellationToken) =>
{
var emailSettings = await GetEmailSettings();
if (emailSettings?.IsComplete() != true)
@ -25,13 +26,21 @@ namespace BTCPayServer.Services.Mails
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
var smtp = emailSettings.CreateSmtpClient();
var mail = new MailMessage(emailSettings.From, email, subject, message)
using (var smtp = emailSettings.CreateSmtpClient())
{
IsBodyHtml = true
};
await smtp.SendMailAsync(mail);
var mail = new MailMessage(emailSettings.From, email, subject, message)
{
IsBodyHtml = true
};
try
{
await smtp.SendMailAsync(mail).WithCancellation(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
smtp.SendAsyncCancel();
}
}
}, TimeSpan.Zero);
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Apps;
using Newtonsoft.Json;
namespace BTCPayServer.Services
@ -10,17 +11,19 @@ namespace BTCPayServer.Services
public class PoliciesSettings
{
[Display(Name = "Requires a confirmation mail for registering")]
public bool RequiresConfirmedEmail
{
get; set;
}
public bool RequiresConfirmedEmail { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[Display(Name = "Disable registration")]
public bool LockSubscription { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[Display(Name = "Discourage search engines from indexing this site")]
public bool DiscourageSearchEngines { get; set; }
[Display(Name = "Display app on website root")]
public string RootAppId { get; set; }
public AppType? RootAppType { get; set; }
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitbankRateProvider : IRateProvider, IHasExchangeName
{
private readonly HttpClient _httpClient;
public BitbankRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => "bitbank";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
.Properties()
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
.ToArray());
}
private static BidAsk CreateBidAsk(JProperty p)
{
var buy = p.Value["buy"].Value<decimal>();
var sell = p.Value["sell"].Value<decimal>();
// Bug from their API (https://github.com/btcpayserver/btcpayserver/issues/741)
return buy < sell ? new BidAsk(buy, sell) : new BidAsk(sell, buy);
}
}
}

View File

@ -23,20 +23,18 @@ namespace BTCPayServer.Services.Rates
public class CoinAverageExchange
{
public CoinAverageExchange(string name, string display)
public CoinAverageExchange(string name, string display, string url)
{
Name = name;
Display = display;
Url = url;
}
public string Name { get; set; }
public string Display { get; set; }
public string Url
{
get
{
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
}
get;
set;
}
}
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
@ -47,7 +45,11 @@ namespace BTCPayServer.Services.Rates
public void Add(CoinAverageExchange exchange)
{
TryAdd(exchange.Name, exchange);
if (!TryAdd(exchange.Name, exchange))
{
this.Remove(exchange.Name);
this.Add(exchange.Name, exchange);
}
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
@ -84,7 +86,6 @@ namespace BTCPayServer.Services.Rates
(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"),
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
(DisplayName: "Bitso", Name: "bitso"),
})
{
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
}
}

View File

@ -39,7 +39,9 @@ namespace BTCPayServer.Services.Rates
lock (notFoundSymbols)
{
var exchangeRates =
rates.Select(t => CreateExchangeRate(t))
rates
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
.Select(t => CreateExchangeRate(t))
.Where(t => t != null)
.ToArray();
return new ExchangeRates(exchangeRates);

View File

@ -39,7 +39,37 @@ namespace BTCPayServer.Services.Rates
static HttpClient _Client = new HttpClient();
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
{
{"ADAXBT","ADAXBT"},
{ "BSVUSD","BSVUSD"},
{ "QTUMEUR","QTUMEUR"},
{ "QTUMXBT","QTUMXBT"},
{ "EOSUSD","EOSUSD"},
{ "XTZUSD","XTZUSD"},
{ "XREPZUSD","XREPZUSD"},
{ "ADAEUR","ADAEUR"},
{ "ADAUSD","ADAUSD"},
{ "GNOEUR","GNOEUR"},
{ "XTZETH","XTZETH"},
{ "XXRPZJPY","XXRPZJPY"},
{ "XXRPZCAD","XXRPZCAD"},
{ "XTZEUR","XTZEUR"},
{ "QTUMETH","QTUMETH"},
{ "XXLMZUSD","XXLMZUSD"},
{ "QTUMCAD","QTUMCAD"},
{ "QTUMUSD","QTUMUSD"},
{ "XTZXBT","XTZXBT"},
{ "GNOUSD","GNOUSD"},
{ "ADAETH","ADAETH"},
{ "ADACAD","ADACAD"},
{ "XTZCAD","XTZCAD"},
{ "BSVEUR","BSVEUR"},
{ "XZECZJPY","XZECZJPY"},
{ "XXLMZEUR","XXLMZEUR"},
{"EOSEUR","EOSEUR"},
{"BSVXBT","BSVXBT"}
});
string[] _Symbols = Array.Empty<string>();
DateTimeOffset? _LastSymbolUpdate = null;

View File

@ -1,57 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class QuadrigacxRateProvider : IRateProvider, IHasExchangeName
{
public const string QuadrigacxName = "quadrigacx";
static HttpClient _Client = new HttpClient();
public string ExchangeName => QuadrigacxName;
private bool TryToBidAsk(JObject p, out BidAsk v)
{
v = null;
JToken bid = p.Property("bid")?.Value;
JToken ask = p.Property("ask")?.Value;
if (bid == null || ask == null)
return false;
if (!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
v1 <= 0m || v2 <= 0m || v1 > v2)
return false;
v = new BidAsk(v1, v2);
return true;
}
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all", cancellationToken);
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
var exchangeRates = new ExchangeRates();
foreach (var prop in rates.Properties())
{
var rate = new ExchangeRate();
if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair))
continue;
rate.CurrencyPair = pair;
rate.Exchange = QuadrigacxName;
if (!TryToBidAsk((JObject)prop.Value, out var v))
continue;
rate.BidAsk = v;
exchangeRates.Add(rate);
}
return exchangeRates;
}
}
}

View File

@ -109,10 +109,10 @@ namespace BTCPayServer.Services.Rates
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
@ -168,9 +168,9 @@ namespace BTCPayServer.Services.Rates
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
return exchanges;
}

View File

@ -0,0 +1,64 @@
using System;
using NBitcoin;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using NBitcoin.Protocol.Connectors;
using NBitcoin.Protocol;
namespace BTCPayServer.Services
{
public class SocketFactory
{
private readonly BTCPayServerOptions _options;
public SocketFactory(BTCPayServerOptions options)
{
_options = options;
}
public async Task<Socket> ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
{
DefaultEndpointConnector connector = new DefaultEndpointConnector();
NodeConnectionParameters connectionParameters = new NodeConnectionParameters();
if (_options.SocksEndpoint != null)
{
connectionParameters.TemplateBehaviors.Add(new NBitcoin.Protocol.Behaviors.SocksSettingsBehavior()
{
SocksEndpoint = _options.SocksEndpoint
});
}
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
try
{
await connector.ConnectSocket(socket, endPoint, connectionParameters, cancellationToken);
}
catch
{
SafeCloseSocket(socket);
}
return socket;
}
internal static void SafeCloseSocket(System.Net.Sockets.Socket socket)
{
try
{
socket.Shutdown(SocketShutdown.Both);
}
catch
{
}
try
{
socket.Dispose();
}
catch
{
}
}
}
}

View File

@ -0,0 +1,90 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
namespace BTCPayServer.Services
{
public class TorServices
{
BTCPayServerOptions _Options;
public TorServices(BTCPayServerOptions options)
{
_Options = options;
}
public TorService[] Services { get; internal set; } = Array.Empty<TorService>();
internal async Task Refresh()
{
if (string.IsNullOrEmpty(_Options.TorrcFile) || !File.Exists(_Options.TorrcFile))
{
if (!string.IsNullOrEmpty(_Options.TorrcFile))
Logs.PayServer.LogWarning("Torrc file is not found");
Services = Array.Empty<TorService>();
return;
}
List<TorService> result = new List<TorService>();
try
{
var torrcContent = await File.ReadAllTextAsync(_Options.TorrcFile);
if (!Torrc.TryParse(torrcContent, out var torrc))
{
Logs.PayServer.LogWarning("Torrc file could not be parsed");
Services = Array.Empty<TorService>();
return;
}
var services = torrc.ServiceDirectories.SelectMany(d => d.ServicePorts.Select(p => (Directory: new DirectoryInfo(d.DirectoryPath), VirtualPort: p.VirtualPort)))
.Select(d => (ServiceName: d.Directory.Name,
ReadingLines: System.IO.File.ReadAllLinesAsync(Path.Combine(d.Directory.FullName, "hostname")),
VirtualPort: d.VirtualPort))
.ToArray();
foreach (var service in services)
{
try
{
var onionHost = (await service.ReadingLines)[0].Trim();
var torService = new TorService()
{
Name = service.ServiceName,
OnionHost = onionHost,
VirtualPort = service.VirtualPort
};
if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase))
torService.ServiceType = TorServiceType.BTCPayServer;
result.Add(torService);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Error while reading hidden service {service.ServiceName} configuration");
}
}
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Error while reading torrc file");
}
Services = result.ToArray();
}
}
public class TorService
{
public TorServiceType ServiceType { get; set; } = TorServiceType.Other;
public string Name { get; set; }
public string OnionHost { get; set; }
public int VirtualPort { get; set; }
}
public enum TorServiceType
{
BTCPayServer,
Other
}
}

View File

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class Torrc
{
public static bool TryParse(string str, out Torrc value)
{
value = null;
List<HiddenServiceDir> serviceDirectories = new List<HiddenServiceDir>();
var lines = str.Split(new char[] { '\n' });
HiddenServiceDir currentDirectory = null;
foreach (var line in lines)
{
if (HiddenServiceDir.TryParse(line, out var dir))
{
serviceDirectories.Add(dir);
currentDirectory = dir;
}
else if (HiddenServicePortDefinition.TryParse(line, out var portDef) && currentDirectory != null)
{
currentDirectory.ServicePorts.Add(portDef);
}
}
value = new Torrc() { ServiceDirectories = serviceDirectories };
return true;
}
public List<HiddenServiceDir> ServiceDirectories { get; set; } = new List<HiddenServiceDir>();
public override string ToString()
{
StringBuilder builder = new StringBuilder();
foreach(var serviceDir in ServiceDirectories)
{
builder.AppendLine(serviceDir.ToString());
foreach (var port in serviceDir.ServicePorts)
builder.AppendLine(port.ToString());
}
return builder.ToString();
}
}
public class HiddenServiceDir
{
public static bool TryParse(string str, out HiddenServiceDir serviceDir)
{
serviceDir = null;
if (!str.Trim().StartsWith("HiddenServiceDir ", StringComparison.OrdinalIgnoreCase))
return false;
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.None);
if (parts.Length != 2)
return false;
serviceDir = new HiddenServiceDir() { DirectoryPath = parts[1].Trim() };
return true;
}
public string DirectoryPath { get; set; }
public List<HiddenServicePortDefinition> ServicePorts { get; set; } = new List<HiddenServicePortDefinition>();
public override string ToString()
{
return $"HiddenServiceDir {DirectoryPath}";
}
}
public class HiddenServicePortDefinition
{
public static bool TryParse(string str, out HiddenServicePortDefinition portDefinition)
{
portDefinition = null;
if (!str.Trim().StartsWith("HiddenServicePort ", StringComparison.OrdinalIgnoreCase))
return false;
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3)
return false;
if (!int.TryParse(parts[1].Trim(), out int virtualPort))
return false;
var addressPort = parts[2].Trim().Split(':', StringSplitOptions.RemoveEmptyEntries);
if (addressPort.Length != 2)
return false;
if (!int.TryParse(addressPort[1].Trim(), out int port))
return false;
if (!IPAddress.TryParse(addressPort[0].Trim(), out IPAddress address))
return false;
portDefinition = new HiddenServicePortDefinition() { VirtualPort = virtualPort, Endpoint = new IPEndPoint(address, port) };
return true;
}
public int VirtualPort { get; set; }
public IPEndPoint Endpoint { get; set; }
public override string ToString()
{
return $"HiddenServicePort {VirtualPort} {Endpoint}";
}
}
}

View File

@ -21,8 +21,13 @@
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
<a href="https://docs.btcpayserver.org/features/apps" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>
<div class="row">
<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>

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@model UpdateCrowdfundViewModel
@{
ViewData["Title"] = "Update Crowdfund";
@ -137,6 +137,11 @@
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="control-label"></label>
<input type="email" asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>

View File

@ -52,18 +52,22 @@
<div class="form-group">
<label asp-for="EnableShoppingCart"></label>
<input asp-for="EnableShoppingCart" type="checkbox" class="form-check" />
<span asp-validation-for="EnableShoppingCart" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ShowDiscount"></label>
<input asp-for="ShowDiscount" type="checkbox" class="form-check" />
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EnableTips"></label>
<input asp-for="EnableTips" type="checkbox" class="form-check" />
<span asp-validation-for="EnableTips" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ButtonText" class="control-label"></label>*
@ -104,6 +108,21 @@
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="control-label"></label>
<input type="email" asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RedirectAutomatically" class="control-label"></label>*
<select asp-for="RedirectAutomatically" asp-items="Model.RedirectAutomaticallySelectList" class="form-control"></select>
<span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" />
</div>

View File

@ -135,7 +135,7 @@
</div>
<div class="col-md-4 col-sm-12">
<partial
name="Crowdfund/ContributeForm"
name="/Views/AppsPublic/Crowdfund/ContributeForm.cshtml"
model="@(new ContributeToCrowdfund()
{
ViewCrowdfundViewModel = Model,

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
@{
@ -10,13 +10,13 @@
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
@if (!Context.Request.Query.ContainsKey("simple"))
{
@ -25,13 +25,15 @@
</script>
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
<script src="~/modal/btcpay.js"></script>
}
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
@Html.Raw(Model.EmbeddedCSS);
</style>
}
@ -39,19 +41,19 @@
<body>
@if (Context.Request.Query.ContainsKey("simple"))
{
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
}
else
{
<noscript>
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
</noscript>
if (Model.AnimationsEnabled)
{
<canvas id="fireworks"></canvas>
}
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml", Model)
}
</body>

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@ -12,7 +12,7 @@
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8" />
<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="apple-touch-icon" href="~/img/icons/icon-512x512.png">
@ -20,20 +20,20 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
@if (Model.EnableShoppingCart)
@if (Model.EnableShoppingCart)
{
<link rel="stylesheet" href="~/cart/css/style.css">
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
<bundle name="wwwroot/bundles/cart-bundle.min.js"/>
}
<style>
.card-deck {
@ -41,9 +41,15 @@
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: .5rem;
}
</style>
.card-deck .card:only-of-type {
max-width: 320px;
margin: auto;
}
</style>
</head>
<body class="h-100">
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
{image}
@ -102,7 +108,7 @@
</tr>
@if (Model.ShowDiscount)
{
<tr>
<tr>
<td colspan="5" class="border-top-0">
<div class="input-group">
<div class="input-group-prepend">
@ -121,10 +127,10 @@
<script id="template-cart-tip" type="text/template">
@if (Model.EnableTips)
{
<tr class="h5">
<tr class="h5">
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
</tr>
<tr>
<tr>
<td colspan="5" class="border-0">
<div class="input-group mb-2">
<div class="input-group-prepend">
@ -136,18 +142,20 @@
</div>
</div>
<div class="row mb-1">
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0) {
@for (int i = 0; i < CustomTipPercentages.Length; i++) {
var percentage = CustomTipPercentages[i];
<div class="col">
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0)
{
@for (int i = 0; i < CustomTipPercentages.Length; i++)
{
var percentage = CustomTipPercentages[i];
<div class="col">
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</div>
}
}
}
</div>
</td>
</tr>
}
</tr>}
</script>
<script id="template-cart-total" type="text/template">
@ -159,54 +167,55 @@
</tr>
</script>
<body class="h-100">
@if (Model.EnableShoppingCart)
{
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
@if (Model.EnableShoppingCart)
{
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
<span aria-hidden="true">
<i class="fa fa-times fa-fw"></i>
</span>
</button>
</div>
<div class="modal-body p-0">
<table id="js-cart-summary" class="table m-0">
<tbody class="my-3">
<tr class="h5">
<td colspan="2" class="border-top-0">Summary</td>
</tr>
<tr class="h6">
<td class="border-0 pb-0">Total products</td>
<td align="right" class="border-0 pb-0">
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr class="h5">
<td colspan="2" class="border-top-0">Summary</td>
</tr>
<tr class="h6">
<td class="border-0 pb-0">Total products</td>
<td align="right" class="border-0 pb-0">
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr class="h6">
<td class="border-0 pb-y">Discount</td>
<td align="right" class="border-0 pb-y">
<span class="js-cart-summary-discount text-nowrap"></span>
</td>
</tr>
}
@if (Model.EnableTips)
{
}
@if (Model.EnableTips)
{
<tr class="h6">
<td class="border-top-0 pt-0">Tip</td>
<td align="right" class="border-top-0 pt-0">
<span class="js-cart-summary-tip text-nowrap"></span>
</td>
</tr>
}
<tr class="h3 table-light">
<td>Total</td>
<td align="right">
<span class="js-cart-summary-total text-nowrap"></span>
</td>
</tr>
}
<tr class="h3 table-light">
<td>Total</td>
<td align="right">
<span class="js-cart-summary-total text-nowrap"></span>
</td>
</tr>
</tbody>
</table>
</div>
@ -214,184 +223,186 @@
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit">
<b>@Model.CustomButtonText</b>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="wrapper">
<!-- Page Content -->
<div id="content">
<div class="p-3">
<div class="row">
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i>&nbsp; <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
</div>
<div class="col-sm-8 col-lg-9 mb-2">
<div class="input-group mb-2">
<input type="text" class="js-search form-control" placeholder="Find product">
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 1049; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
</div>
</div>
<div class="wrapper">
<!-- Page Content -->
<div id="content">
<div class="p-3">
<div class="row">
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
<a class="js-cart btn btn-warning text-white text-right" href="#">
<i class="fa fa-shopping-basket"></i>&nbsp;
<span class="badge badge-light badge-pill">
<span id="js-cart-items">0</span>
</span>
</a>
</div>
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row">
@{
var rowsJ = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
}
@for (int x = 0; x < rowsJ; x++)
{
var startingIndex = x == 0 ? 0 : x * 4;
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
<div class="row card-deck my-3 mx-auto">
@for (int i = startingIndex; i < endLoopIndex; i++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="js-add-cart card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
}
<div class="col-sm-8 col-lg-9 mb-2">
<div class="input-group mb-2">
<input type="text" class="js-search form-control" placeholder="Find product">
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute; right: 0px; z-index: 1049; display: none;">
<i class="fa fa-times-circle fa-lg"></i>
</a>
</div>
}
</div>
</div>
</div>
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark">
<div class="bg-warning p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row card-deck my-3 mx-auto">
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
<thead class="thead-dark">
<tr>
<th colspan="3" width="55%">Product</th>
<th class="text-center" width="20%"><div style="width: 84px">Quantity</div></th>
<th class="text-right" width="25%"><div style="min-width: 50px">Price</div></th>
</tr>
</thead>
<tbody></tbody>
</table>
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
<tbody></tbody>
</table>
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
<div class="text-center mb-5 pb-5">
<img src="~/img/logo-white.png" height="40">
</div>
</nav>
</div>
} else {
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
@{
var rows = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
}
@for (int x = 0; x < rows; x++)
{
var startingIndex = x == 0 ? 0 : x * 4;
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
<div class="row card-deck my-3 mx-auto">
@for (int i = startingIndex; i < endLoopIndex; i++)
@for (var index = 0; index < Model.Items.Length; index++)
{
var item = Model.Items[i];
var item = Model.Items[index];
var image = item.Image;
var description = item.Description;
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
<div class="js-add-cart card my-2 card-wrapper" data-id="@index">
@if (!String.IsNullOrWhiteSpace(image))
{
@:<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<img class="card-img-top" src="@image" alt="Card image cap">
<p class="card-text">@description</p>
}
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
@if (item.Custom && !Model.EnableShoppingCart)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
<div class="card-footer pt-0 bg-transparent border-0">
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
}
</div>
</div>
</div>
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark">
<div class="bg-warning p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#">
<i class="fa fa-times fa-lg"></i>
</a>
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
</div>
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
<thead class="thead-dark">
<tr>
<th colspan="3" width="55%">Product</th>
<th class="text-center" width="20%">
<div style="width: 84px">Quantity</div>
</th>
<th class="text-right" width="25%">
<div style="min-width: 50px">Price</div>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
<tbody></tbody>
</table>
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit">
<b>Confirm</b>
</button>
<div class="text-center mb-5 pb-5">
<img src="~/img/logo-white.png" height="40">
</div>
</nav>
</div>
}
else
{
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
<div class="row card-deck my-3 mx-auto">
@for (int x = 0; x < Model.Items.Length; x++)
{
var item = Model.Items[x];
<div class="card my-2" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image))
{
<img class="card-img-top" src="@item.Image" alt="Card image cap">
}
<div class="card-body pb-0">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(item.Description))
{
<p class="card-text">@item.Description</p>
}
</div>
<div class="card-footer bg-transparent border-0">
@if (item.Custom)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
}
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
<div class="card">
<div class="card-body">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<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">@Model.CustomButtonText</button></div>
</div>
</form>
<div class="card my-2">
<div class="card-body my-auto pb-0">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
</div>
<div class="card-footer bg-transparent border-0">
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<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">@Model.CustomButtonText</button>
</div>
</div>
</div>
</form>
</div>
</div>
}
</div>
</div>
}
</div>
}
</body>
</html>

View File

@ -48,7 +48,7 @@
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
<span class="payment__currencies ">
<img v-bind:src="srvModel.cryptoImage" />
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
</span>
@ -73,7 +73,7 @@
{
<div class="payment__currencies_noborder">
<img v-bind:src="srvModel.cryptoImage" />
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
</div>
}
@ -101,7 +101,7 @@
</div>
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
</div>
</div>
@ -207,7 +207,7 @@
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon"
v-if="scanDisplayQr" />
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }"
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"
v-if="scanDisplayQr">
</qrcode>
@ -303,7 +303,7 @@
:mode="srvModel.coinSwitchMode"
:merchant-id="srvModel.coinSwitchMerchantId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.btcDue"
:to-currency-due="coinswitchAmountDue"
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
:to-currency-address="srvModel.btcAddress">
<div>

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.Services.LanguageService langService
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@ -51,9 +51,9 @@
<p>Alternatively, click below to continue to our HTML-only invoice.</p>
<form action="/invoice-noscript" method="GET">
<button style="text-decoration: underline; color: blue">Continue to javascript-disabled invoice &gt;</button>
</form>
<a href="/invoice-noscript?id=@Model.InvoiceId" style="text-decoration: underline; color: blue">
Continue to javascript-disabled invoice &gt;
</a>
</center>
</noscript>
@ -176,7 +176,15 @@
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal,
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
selectedThirdPartyProcessor: ""
},
computed: {
coinswitchAmountDue: function() {
return this.srvModel.coinSwitchAmountMarkupPercentage
? (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
: this.srvModel.btcDue;
}
}
});
</script>

View File

@ -0,0 +1,75 @@
@model PaymentModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<META NAME="robots" CONTENT="noindex,nofollow">
<title>@Model.HtmlTitle</title>
</head>
<body>
<h1>Pay with @Model.StoreName</h1>
@if (Model.Status == "new")
{
<div>
<p>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
<p>Time remaining: @Model.TimeLeft</p>
<p><a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;">@Model.InvoiceBitcoinUrl</a></p>
@if (Model.IsLightning)
{
<p>Peer Info: <b>@Model.PeerInfo</b></p>
}
</div>
@if (Model.AvailableCryptos.Count > 1)
{
<div>
<hr />
<h2>Pay with:</h2>
<ul style="list-style-type: none;padding-left: 5px;">
@foreach (var crypto in Model.AvailableCryptos)
{
<li style="height: 32px; line-height: 32px;">
<a href="/invoice-noscript?id=@Model.InvoiceId&paymentMethodId=@crypto.PaymentMethodId">
<img alt="@crypto.PaymentMethodName" src="@crypto.CryptoImage" style="vertical-align:middle; height:24px; text-decoration:none; margin-top: -3px;" /></a>
<a href="/invoice-noscript?id=@Model.InvoiceId&paymentMethodId=@crypto.PaymentMethodId" style="padding-top: 2px;">
@crypto.PaymentMethodName
@(crypto.IsLightning ? Html.Raw("&#9889;") : null)
(@crypto.CryptoCode)
</a>
</li>
}
</ul>
</div>
}
}
else if (Model.Status == "paid" || Model.Status == "complete" || Model.Status == "confirmed")
{
<div>
<p>This invoice has been paid</p>
</div>
}
else if (Model.Status == "expired" || Model.Status == "invalid")
{
<div>
<p>This invoice has expired</p>
</div>
}
else
{
<div>
<p>Non-supported state of invoice</p>
</div>
}
<hr />
<p>
<a href="/i/@Model.InvoiceId">Go back to Javascript enabled invoice</a>
</p>
</body>
</html>

View File

@ -48,7 +48,9 @@
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
<a href="https://docs.btcpayserver.org/features/accounting" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<a href="https://docs.btcpayserver.org/features/accounting" target="_blank">
<span class="fa fa-question-circle-o" title="More information..."></span>
</a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a asp-action="Export" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">CSV</a>
<a asp-action="Export" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">JSON</a>
@ -77,7 +79,12 @@
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Date</th>
<th style="min-width: 90px;" class="col-md-auto">
Date
<a href="javascript:switchTimeFormat()">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th>OrderId</th>
<th>InvoiceId</th>
<th>Status</th>
@ -89,7 +96,11 @@
@foreach (var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
@invoice.Date.ToBrowserDate()
</span>
</td>
<td>
@if (invoice.RedirectUrl != string.Empty)
{
@ -142,31 +153,69 @@
</tbody>
</table>
<nav aria-label="...">
<ul class="pagination">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})">Previous</a>
<nav aria-label="..." class="w-100">
<ul class="pagination float-left">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@listInvoices(-1, Model.Count)">&laquo;</a>
</li>
<li class="page-item disabled">
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Invoices.Count) of @Model.Total</span>
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Invoices.Count) ? null : "disabled")">
<a class="page-link" href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">Next</a>
<a class="page-link" href="@listInvoices(1, Model.Count)">&raquo;</a>
</li>
</ul>
<ul class="pagination float-right">
<li class="page-item disabled">
<span class="page-link">Page Size:</span>
</li>
<li class="page-item @(Model.Count == 50 ? "active" : null)">
<a class="page-link" href="@listInvoices(0, 50)">50</a>
</li>
<li class="page-item @(Model.Count == 100 ? "active" : null)">
<a class="page-link" href="@listInvoices(0, 100)">100</a>
</li>
<li class="page-item @(Model.Count == 250 ? "active" : null)">
<a class="page-link" href="@listInvoices(0, 250)">250</a>
</li>
<li class="page-item @(Model.Count == 500 ? "active" : null)">
<a class="page-link" href="@listInvoices(0, 500)">500</a>
</li>
</ul>
</nav>
@{
string listInvoices(int prevNext, int count)
{
var skip = Model.Skip;
if (prevNext == -1)
skip = Math.Max(0, Model.Skip - Model.Count);
else if (prevNext == 1)
skip = Model.Skip + count;
var act = Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = skip,
count = count,
});
return act;
}
}
</div>
</div>
<script type="text/javascript">
function switchTimeFormat() {
$(".switchTimeFormat").each(function (index) {
var htmlVal = $(this).html();
var switchVal = $(this).attr("data-switch");
$(this).html(switchVal);
$(this).attr("data-switch", htmlVal);
});
}
</script>
</section>

View File

@ -1,6 +1,6 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
<section>
<div class="container">

View File

@ -23,6 +23,7 @@
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
<a href="https://docs.btcpayserver.org/features/paymentrequests" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>

View File

@ -1,6 +1,6 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@{
ViewData["Title"] = Model.Title;
@ -11,13 +11,13 @@
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
@if (!Context.Request.Query.ContainsKey("simple"))
{
@ -26,13 +26,15 @@
</script>
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
<script src="~/modal/btcpay.js"></script>
}
<bundle name="wwwroot/bundles/payment-request-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
@Html.Raw(Model.EmbeddedCSS);
</style>
}
</head>

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
@ -10,7 +10,7 @@
<html>
<head>
<title>@Model.CryptoCode LN Node Info</title>
<meta charset="utf-8"/>
<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="apple-touch-icon" href="~/img/icons/icon-512x512.png">
@ -18,10 +18,10 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
@ -74,70 +74,76 @@
text-align: center;
}
.copy { cursor: copy; }
.qr-container svg {
width: 256px;
height: 256px;
}
.copy {
cursor: copy;
}
</style>
</head>
<body >
<noscript>
<div class="container">
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<body>
<noscript>
<div class="container">
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
<small class="@(Model.Available ? "text-success" : "text-danger")">
<span class="fa fa-circle "></span>
</small>
</h1>
@if (Model.Available)
{
<div class="card-body m-sm-0 p-sm-0">
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
<small class="@(Model.Available ? "text-success" : "text-danger")">
<span class="fa fa-circle "></span>
</small>
</h1>
@if (Model.Available)
{
<div class="card-body m-sm-0 p-sm-0">
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info" />
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
</div>
</div>
}
}
</div>
</div>
</div>
</div>
</div>
</div>
</noscript>
<div id="app" class="container">
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
{{srvModel.cryptoCode}} Lightning Node
- {{srvModel.available? "Online" : "Unavailable"}}
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
<span class="fa fa-circle "></span>
</small>
</h1>
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon"/>
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</noscript>
<div id="app" class="container">
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
{{srvModel.cryptoCode}} Lightning Node
- {{srvModel.available? "Online" : "Unavailable"}}
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
<span class="fa fa-circle "></span>
</small>
</h1>
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info" />
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -28,7 +28,7 @@
{
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://www.pebble.indiesquare.me/ target="_blank"">
<a href="https://www.pebble.indiesquare.me/" target="_blank">
<img src="~/img/pebblewallet.jpg" height="100" />
</a>
<p><a href="https://www.pebble.indiesquare.me/" target="_blank">Pebble</a></p>

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