Compare commits

...

174 Commits

Author SHA1 Message Date
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
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
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
fafb02b0dc bump 2019-03-11 15:20:10 +09:00
53cecc8c8a Improve error message for POST /tokens without body 2019-03-10 15:10:30 +09:00
db0e9ee8f8 Merge pull request #654 from Kukks/robots-suck
Add policy to discourage search engines + build robots.txt dynamcally
2019-03-10 15:00:11 +09:00
2fac794d96 Merge pull request #667 from pavlenex/pavlenex-patch-issue-templates
Add Issue Templates
2019-03-10 14:58:58 +09:00
ffcd716906 Create feature_request.md 2019-03-09 23:21:03 +01:00
85f50724db Create Bug Report template 2019-03-09 23:16:29 +01:00
808a995741 Merge pull request #665 from rockstardev/rock-qrcodefix
Rendering QR code only if there is data, plus loading indicator
2019-03-10 00:40:07 +09:00
4deb853914 remo useless mode now 2019-03-09 16:35:53 +01:00
470ec3354e Rendering QR code only if there is data, plus loading indicator 2019-03-09 09:33:05 -06:00
053c2da9f1 use thememanager instead of view component 2019-03-09 16:29:04 +01:00
c0e28ce66e Revert "Remove error in console logs on checkout page"
This reverts commit 08dd94e267c4810837b2fccd33b9001a3e406b30.
2019-03-10 00:28:47 +09:00
08dd94e267 Remove error in console logs on checkout page 2019-03-10 00:18:34 +09:00
7497865d1f Pay button was not working properly if the server was not en-US (Fix #638) 2019-03-09 23:40:22 +09:00
1888e4fe2b remove robots and remove nofollow 2019-03-09 15:01:56 +01:00
6746a5cbd5 add meta for noindex,nofollow if policy set 2019-03-09 14:55:37 +01:00
baecb7bb0c Fix coinswitch issue (#664)
Coinswitch did a breaking change, this fixes it. Closes #660
2019-03-09 22:36:25 +09:00
28bf4b42bb Fix tests 2019-03-09 22:36:07 +09:00
c73dc425ad Remove externalurl in command line options --help (Fix #658) 2019-03-09 22:30:49 +09:00
2138b7dcb8 Fix invoice popup not showing up if btcpay has a rootpath 2019-03-09 22:28:20 +09:00
8b6c4a9383 simplifed robots generator 2019-03-09 14:23:55 +01:00
344755cbd0 Add policy to discourage search engines + build robots.txt dynamcally
closes #390
2019-03-09 14:13:10 +01:00
63a975267c Fix coinswitch issue
Coinswitch did a breaking change, this fixes it. Closes #660
2019-03-09 14:11:56 +01:00
3c7d93e88d No ROOTPATH in launchsettings 2019-03-09 18:40:41 +09:00
75974037bc Fix SignalR paths if RootPath is used 2019-03-09 16:08:31 +09:00
e8a346182b Fix vue-qrcode inside the lightning network info vue 2019-03-09 12:51:05 +09:00
e96d34f741 New QR Code component, fixes scanning of long lightning invoices 2019-03-08 16:19:39 -06:00
7dad814f19 Fix some checkout page if RootPath is set (#659) 2019-03-09 00:48:33 +09:00
7e67ca1413 Fix RootPath was not working correctly on Linux 2019-03-08 23:38:00 +09:00
603263549b Document LND supported wallets in services (#657)
* Document LND supported wallets in services

* Add Zeus

* Open links in new tab
2019-03-08 18:12:14 +09:00
a82b971ce7 bump 2019-03-07 20:51:51 +09:00
b58c8ef2f0 Bump libraries 2019-03-07 20:33:05 +09:00
274533bfdf Fix spot not using HttpClient created by the httpclientfactory 2019-03-07 19:41:20 +09:00
cd6ce401e1 Fix logs getting spammed by HTTP requests 2019-03-07 19:41:19 +09:00
0c0809101d Datetime picker and small edit UI changes (#647)
* do not allow negative amounts for crowdfund and payment requests

* remove currency placeholder in payment requests

* Improve date picker ui 

Clear button only appears when a value is set. If no value is set, display a placeholder indicating it. closes #625
2019-03-07 14:29:29 +09:00
4b342376a8 Pos experimental card deck (#651)
* Try out experimental card deck

* apply card deck to shopping cart version
2019-03-07 14:28:42 +09:00
fd963b9ad0 fix no store error message for payment request (#646)
* fix no store error message for payment request 

closes #628

* Update PaymentRequestController.cs
2019-03-07 14:28:14 +09:00
06406c0695 Clone Payment Requests (#648)
* Clone Payment Requests

closes #615

* Do not save clone instantly
2019-03-07 14:27:16 +09:00
2b567de5c1 Allow sounds and animation colors in crowdfund to be configured (#653)
closes #652
2019-03-07 14:25:09 +09:00
ef46d03760 fix lightning typo (#655)
closes #622
2019-03-07 13:14:47 +09:00
b174f299fa Fix LND QR code (Fix #656) 2019-03-07 10:26:27 +09:00
09837966b9 bump 2019-03-06 17:53:03 +09:00
465dce1d02 Running check of submitted SMTP data on both Test and Save 2019-03-05 13:00:14 -06:00
067dbad546 Fix build 2019-03-05 17:29:52 +09:00
522970fdb9 Fix build 2019-03-05 17:21:44 +09:00
e67aa499a6 Fix build 2019-03-05 17:20:26 +09:00
3b68d81507 Small refactoring 2019-03-05 17:13:34 +09:00
051248f2fc Add CancellationToken to GetRatesAsync and propagate it from the controllers to the rate fetcher 2019-03-05 17:09:17 +09:00
9a239f99f4 Add bylls as supported exchange 2019-03-05 16:07:23 +09:00
86c431d66e Only get contributions from invoices matching the currency of the Payment request or crowdfund 2019-03-05 14:26:27 +09:00
b35fe0e8e3 Fix CurrentPendingAmount/CurrentAmount not being set 2019-03-05 14:06:40 +09:00
54905f5ceb Fix currencyValue 2019-03-05 13:58:13 +09:00
1c9b05d992 Fix: Payment requests and crowdfund were estimating current contributions based on the current rate 2019-03-05 13:54:34 +09:00
a89c71df38 Fix bundleconfig 2019-03-04 22:38:06 +09:00
3db1f2af12 Fix warning when compiling bundles 2019-03-04 22:35:56 +09:00
5399ff2751 Fix forwarding options 2019-03-04 22:34:14 +09:00
bcea6027e9 Replace Forwarded Headers via ASP.NET Core middleware 2019-03-04 20:48:19 +09:00
a9722df7e4 bump 2019-03-04 18:39:16 +09:00
474be6f7be French update 2019-03-04 18:39:03 +09:00
ada9a7264b Fix typo in AppsController.cs (#630)
Fixing a tiny typo in the AppsController.cs
2019-03-04 18:36:47 +09:00
e991b302d0 Fix bug: "BTCPay is expecting you to access this website" being cached 2019-03-04 18:33:57 +09:00
eef301c6ec Fix error message if X-Forwarded-Proto not set correctly 2019-03-04 17:26:08 +09:00
3fdfd0adfd Removed exponentiation on invoice request amount
Fixes #620
2019-03-03 17:44:28 -06:00
358f1ffc43 Removed border when there is only one currency 2019-03-03 17:44:28 -06:00
349c3409df Preserve password when sending test email 2019-03-03 17:40:40 -06:00
0263a2950c added enabled to ViewCrowdfundViewModel, added warning on preview page 2019-03-03 17:39:53 -06:00
0364a57cae Added a space between Enable and SSL. 2019-03-03 17:39:22 -06:00
e232dd7d7e Add a space between "Test" and "Email" in the UI. 2019-03-03 17:38:57 -06:00
fee936b569 Only checking for last admin if user being deleted is admin
Bugfixing issue #632
2019-03-03 17:38:38 -06:00
420115c54d bump nbx 2019-03-03 01:37:34 +09:00
312e961098 Update c-lightning, nbxplorer and lightning libs 2019-03-02 23:38:26 +09:00
223213857f Do not expose internal IP on SSH connection settings 2019-03-01 16:41:36 +09:00
168 changed files with 13315 additions and 2772 deletions

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Report a problem
about: File a technical problem or report a bug
---
**Describe the problem/bug**
A clear and concise description of what the bug is.
**Your environment**
* Version of BTCPay Server:
* Deployment method:
* Other relevant environment details:
**Logs (if applicable)**
Basic logs can be found in Server Settings > Logs.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
Tell us what happens instead
**Screenshots/Links**
If applicable, add screenshots or links to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Ideas and feature requests
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Provide examples**
If applicable provide examples, wireframes, sketches or images to better explain your idea.
**Additional context**
Add any other context or screenshots about the feature request here.

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()
@ -129,6 +131,7 @@ namespace BTCPayServer.Tests
{
s.AddLogging(l =>
{
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
@ -208,6 +211,8 @@ namespace BTCPayServer.Tests
}
}
public HttpClient HttpClient { get; set; }
public string HostName
{
get;

View File

@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
.Value);
}
}

View File

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
}, default));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
@ -118,7 +118,7 @@ namespace BTCPayServer.Tests
{
RedirectToCheckout = false,
Amount = new decimal(0.01)
}));
}, default));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
@ -130,7 +130,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
}, default));
//Scenario 4: Enabled But End Date < Now - Not Allowed
@ -142,7 +142,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
}, default));
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
@ -156,13 +156,13 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
}));
}, default));
//Scenario 6: Allowed
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.05)
}));
}, default));
}
}

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

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

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);
@ -589,7 +631,7 @@ namespace BTCPayServer.Tests
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule).GetAwaiter().GetResult();
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult();
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
@ -718,20 +760,32 @@ namespace BTCPayServer.Tests
acc.RegisterDerivationScheme("LTC");
var rateController = acc.GetController<RateController>();
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId)
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetBaseCurrencyRatesResult);
Assert.NotNull(GetBaseCurrencyRatesResult.Data);
Assert.Equal(2, GetBaseCurrencyRatesResult.Data.Length);
Assert.Single(GetBaseCurrencyRatesResult.Data.Where(o => o.Code == "LTC"));
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId)
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);
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId)
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate>>();
Assert.NotNull(GetCurrencyPairRateResult);
@ -869,7 +923,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
foreach(var req in new[]
foreach (var req in new[]
{
"invoices/",
"invoices",
@ -1006,7 +1060,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 +1075,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 +1141,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 +1240,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 +1248,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 +1265,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 +1283,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 +1555,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 1000)]
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
@ -1612,21 +1710,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 +1733,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]
@ -2239,7 +2336,7 @@ donation:
foreach (var result in factory
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
@ -2251,17 +2348,25 @@ 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", "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().GetAwaiter().GetResult();
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
}
[Fact]
@ -2277,7 +2382,7 @@ donation:
.ToHashSet();
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = fetcher.FetchRates(pairs, rules);
var result = fetcher.FetchRates(pairs, rules, default);
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
@ -2299,7 +2404,7 @@ donation:
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync()
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
Hit = true;
var rates = new ExchangeRates();
@ -2351,7 +2456,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));
@ -2406,47 +2511,47 @@ donation:
var fetcher = new RateFetcher(factory);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
fetch.UpdateIfNecessary(default).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
fetch.UpdateIfNecessary(default).GetAwaiter().GetResult();
spy.AssertHit();
fetch.GetRatesAsync().GetAwaiter().GetResult();
fetch.GetRatesAsync(default).GetAwaiter().GetResult();
Thread.Sleep(1000);
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult());
}
[Fact]

View File

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.8
image: nicolasdorier/nbxplorer:2.0.0.26
restart: unless-stopped
ports:
- "32838:32838"
@ -119,7 +119,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.6.2-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -165,7 +165,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.6.2-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.76</Version>
<Version>1.0.3.92</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.9" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.15" />
<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.84" />
<PackageReference Include="NBitcoin" Version="4.1.1.98" />
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.4" />
<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

@ -32,7 +32,6 @@ namespace BTCPayServer.Configuration
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
@ -41,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

@ -35,6 +35,8 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
if (request == null)
throw new BitpayHttpException(400, "The request body is missing");
PairingCodeEntity pairingEntity = null;
if (string.IsNullOrEmpty(request.PairingCode))
{

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
@ -56,7 +57,9 @@ namespace BTCPayServer.Controllers
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View(vm);
}
@ -90,6 +93,24 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
}
var parsedSounds = vm.Sounds.Split(
new[] {"\r\n", "\r", "\n"},
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
}
var parsedAnimationColors = vm.AnimationColors.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
}
if (!ModelState.IsValid)
{
@ -115,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,
@ -124,7 +146,9 @@ namespace BTCPayServer.Controllers
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
};
app.TagAllInvoices = vm.UseAllStoreInvoices;

View File

@ -77,6 +77,8 @@ namespace BTCPayServer.Controllers
public string CustomCSSLink { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
}
[HttpGet]
@ -101,7 +103,9 @@ 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
};
if (HttpContext?.Request != null)
{

View File

@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
@ -104,7 +104,7 @@ namespace BTCPayServer.Controllers
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));

View File

@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
@ -108,11 +109,13 @@ namespace BTCPayServer.Controllers
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
if (settings.Enabled) return View(appInfo);
if(!isAdmin)
return NotFound();
return View(await _AppService.GetAppInfo(appId));
return View(appInfo);
}
[HttpPost]
@ -120,7 +123,7 @@ namespace BTCPayServer.Controllers
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
@ -131,14 +134,12 @@ namespace BTCPayServer.Controllers
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled)
{
if (!isAdmin)
return NotFound("Crowdfund is not currently active");
if (!settings.Enabled && !isAdmin) {
return NotFound("Crowdfund is not currently active");
}
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(this.Request);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
@ -183,10 +184,11 @@ namespace BTCPayServer.Controllers
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
NotificationEmail = settings.NotificationEmail,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) }, cancellationToken: cancellationToken);
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
@ -216,7 +218,7 @@ namespace BTCPayServer.Controllers
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null)
string posData = null, CancellationToken cancellationToken = default)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -261,11 +263,12 @@ namespace BTCPayServer.Controllers
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
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());
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}

View File

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Payments.Changelly;
@ -48,7 +49,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("calculate")]
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
decimal toCurrencyAmount, CancellationToken cancellationToken)
{
try
{
@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
{
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
}
var callCounter = 0;
@ -102,11 +103,11 @@ namespace BTCPayServer.Controllers
}
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
decimal toCurrencyAmount, CancellationToken cancellationToken)
{
var store = HttpContext.GetStoreData();
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
if (rate.BidAsk == null) return BadRequest();
var flatRate = rate.BidAsk.Center;
return Ok(flatRate * toCurrencyAmount);

View File

@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using System.Net.Http;
using Newtonsoft.Json.Linq;

View File

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
@ -32,11 +33,11 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
{
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}
[HttpGet]

View File

@ -188,7 +188,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 +213,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);
@ -273,6 +290,7 @@ namespace BTCPayServer.Controllers
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
PaymentMethodId = paymentMethodId.ToString(),
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
@ -283,6 +301,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(),
@ -314,6 +333,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,
@ -345,9 +365,8 @@ namespace BTCPayServer.Controllers
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
return "/" + res;
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
}
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
@ -437,7 +456,7 @@ namespace BTCPayServer.Controllers
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok();
return Ok("{}");
}
[HttpGet]
@ -542,7 +561,7 @@ namespace BTCPayServer.Controllers
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
@ -590,7 +609,7 @@ namespace BTCPayServer.Controllers
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
@ -62,7 +63,7 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
@ -104,7 +105,7 @@ namespace BTCPayServer.Controllers
}
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
@ -137,7 +138,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)));
}
@ -154,7 +155,7 @@ namespace BTCPayServer.Controllers
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
@ -199,8 +200,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" };
@ -229,6 +244,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)];
@ -241,8 +257,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;
@ -270,7 +291,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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
@ -23,6 +24,7 @@ using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using NBitpayClient;
namespace BTCPayServer.Controllers
@ -97,7 +99,12 @@ namespace BTCPayServer.Controllers
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage = "Error: You need to create at least one store before creating a payment request"
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}
});
}
@ -175,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"
});
}
@ -197,7 +204,7 @@ namespace BTCPayServer.Controllers
new
{
StatusMessage =
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
});
}
}
@ -212,7 +219,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
return View(result);
}
@ -220,14 +227,14 @@ namespace BTCPayServer.Controllers
[Route("{id}/pay")]
[AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null)
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)
@ -267,21 +274,11 @@ namespace BTCPayServer.Controllers
}
if (result.AllowCustomPaymentAmounts && amount != null)
{
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
}
amount = Math.Min(result.AmountDue, amount.Value);
else
amount = result.AmountDue;
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
}
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
bool redirectToInvoice,
ViewPaymentRequestViewModel result,
decimal? amount = null)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
var blob = pr.GetBlob();
var store = pr.StoreData;
@ -291,14 +288,17 @@ namespace BTCPayServer.Controllers
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.GetValueOrDefault(result.AmountDue),
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = redirectUrl,
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.Value,
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = redirectUrl,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
cancellationToken: cancellationToken))
.Data.Id;
if (redirectToInvoice)
{
@ -313,9 +313,28 @@ namespace BTCPayServer.Controllers
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
[HttpGet]
[Route("{id}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string id)
{
var result = await EditPaymentRequest(id);
if (result is ViewResult viewResult)
{
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
model.Id = null;
model.Title = $"Clone of {model.Title}";
return View("EditPaymentRequest", model);
}
return NotFound();
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
@ -28,7 +29,7 @@ namespace BTCPayServer.Controllers
[MediaTypeAcceptConstraintAttribute("text/html")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
@ -56,7 +57,7 @@ namespace BTCPayServer.Controllers
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return Redirect(invoice.Data.Url);
}
}

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

@ -13,6 +13,7 @@ using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
using System.Threading;
namespace BTCPayServer.Controllers
{
@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers
[Route("rates/{baseCurrency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
var store = this.HttpContext.GetStoreData();
@ -64,7 +65,7 @@ namespace BTCPayServer.Controllers
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, store.Id);
var result = await GetRates2(currencypairs, store.Id, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
@ -75,10 +76,10 @@ namespace BTCPayServer.Controllers
[Route("rates/{baseCurrency}/{currency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
@ -88,10 +89,9 @@ namespace BTCPayServer.Controllers
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
var result = await GetRates2(currencyPairs, storeId);
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
if (storeId == null)
@ -139,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;
}
@ -168,7 +163,7 @@ namespace BTCPayServer.Controllers
pairs.Add(pair);
}
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
var fetching = _RateProviderFactory.FetchRates(pairs, rules, cancellationToken);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))

View File

@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
private readonly TorServices _torServices;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
@ -48,6 +49,7 @@ namespace BTCPayServer.Controllers
NBXplorerDashboard dashBoard,
IHttpClientFactory httpClientFactory,
LightningConfigurationProvider lnConfigProvider,
TorServices torServices,
Services.Stores.StoreRepository storeRepository)
{
_Options = options;
@ -58,6 +60,7 @@ namespace BTCPayServer.Controllers
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
_torServices = torServices;
}
[Route("server/rates")]
@ -246,6 +249,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();
@ -384,18 +394,17 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
"Delete"));
@ -428,13 +437,6 @@ namespace BTCPayServer.Controllers
}
public IHttpClientFactory HttpClientFactory { get; }
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
@ -471,6 +473,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);
}
@ -565,7 +586,7 @@ namespace BTCPayServer.Controllers
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
model.QRCodeLink = Url.Action(nameof(GetLNDConfig), new { configKey = configKey });
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
model.QRCode = $"config={model.QRCodeLink}";
}
}
@ -658,9 +679,11 @@ namespace BTCPayServer.Controllers
return NotFound();
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
}
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}@{settings.Server}{port}";
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
vm.Password = settings.Password;
vm.KeyFilePassword = settings.KeyFilePassword;
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
@ -682,19 +705,28 @@ namespace BTCPayServer.Controllers
return View(settings);
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
if (command == "Test")
{
try
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
var client = model.Settings.CreateSmtpClient();
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";

View File

@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
@ -77,7 +78,7 @@ namespace BTCPayServer.Controllers
case "test":
try
{
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl);
var result = await client.GetCurrenciesFull();
vm.StatusMessage = "Test Successful";

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
@ -188,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)
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);
@ -219,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))
@ -267,7 +283,7 @@ namespace BTCPayServer.Controllers
pairs.Add(currencyPair);
}
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken);
var testResults = new List<RatesViewModel.TestResultViewModel>();
foreach (var fetch in fetchs)
{
@ -332,13 +348,14 @@ 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;
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
@ -395,13 +412,14 @@ 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;
if (StoreData.SetStoreBlob(blob))
{
needUpdate = true;

View File

@ -181,7 +181,7 @@ namespace BTCPayServer.Controllers
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;

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

View File

@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -17,7 +18,7 @@ namespace BTCPayServer
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
NBXplorerDashboard _Dashboard;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
public ExplorerClientProvider(IHttpClientFactory httpClientFactory, BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
_NetworkProviders = networkProviders;
@ -32,14 +33,15 @@ namespace BTCPayServer
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
if (setting.ExplorerUri != null)
{
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
}
}
}
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
private static ExplorerClient CreateExplorerClient(HttpClient httpClient, BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
explorer.SetClient(httpClient);
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");

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(
@ -217,8 +243,10 @@ namespace BTCPayServer
/// <returns></returns>
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
{
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
uri.IsAbsoluteUri)
return path;
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
@ -301,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

@ -45,10 +45,12 @@ namespace BTCPayServer.HostedServices
}
public bool ShowRegister { get; set; }
public bool DiscourageSearchEngines { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
DiscourageSearchEngines = data.DiscourageSearchEngines;
}
}

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

@ -333,6 +333,8 @@ namespace BTCPayServer.HostedServices
public Task StopAsync(CancellationToken cancellationToken)
{
if (_Cts == null)
return Task.CompletedTask;
leases.Dispose();
_Cts.Cancel();
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;

View File

@ -1,4 +1,5 @@
using System;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -47,7 +48,7 @@ namespace BTCPayServer.HostedServices
{
await Task.WhenAll(_RateProviderFactory.Providers
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
{
if (t.Result.Exception != null)
{
@ -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,8 @@ namespace BTCPayServer.Hosting
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<TorServices>();
services.TryAddSingleton<SocketFactory>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<InvoiceRepository>(o =>
@ -188,6 +190,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 +218,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

@ -32,8 +32,6 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
@ -90,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;
@ -97,7 +98,7 @@ namespace BTCPayServer.Hosting
if (
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
(path == "/invoices" || path == "/invoices/") &&
(isCors || (method == "POST" && isJson)))
return true;
@ -125,57 +126,6 @@ namespace BTCPayServer.Hosting
return false;
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
string reverseProxyScheme = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
{
var scheme = proto.SingleOrDefault();
if (scheme != null)
{
reverseProxyScheme = scheme;
}
}
ushort? reverseProxyPort = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
{
var portString = port.SingleOrDefault();
if (portString != null && ushort.TryParse(portString, out ushort pp))
{
reverseProxyPort = pp;
}
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
ushort? p = null;
if (reverseProxyScheme != null)
{
httpContext.Request.Scheme = reverseProxyScheme;
if (reverseProxyScheme == "http")
p = 80;
if (reverseProxyScheme == "https")
p = 443;
}
if (reverseProxyPort != null)
{
p = reverseProxyPort.Value;
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;

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

@ -3,7 +3,6 @@ using System.Reflection;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
@ -14,9 +13,9 @@ using BTCPayServer.Authentication;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using BTCPayServer.Services;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.HttpOverrides;
using BTCPayServer.Data;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
@ -35,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;
@ -159,14 +157,22 @@ namespace BTCPayServer.Hosting
app.UseDeveloperExceptionPage();
}
var forwardingOptions = new ForwardedHeadersOptions()
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
};
forwardingOptions.KnownNetworks.Clear();
forwardingOptions.KnownProxies.Clear();
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
app.UseForwardedHeaders(forwardingOptions);
app.UseCors();
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseSignalR(route =>
{
route.MapHub<AppHub>("/apps/hub");
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
AppHub.Register(route);
PaymentRequestHub.Register(route);
});
app.UseWebSockets();
app.UseStatusCodePages();

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

@ -0,0 +1,99 @@
// Copied and adjusted from https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DecimalModelBinder.cs
using System;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace BTCPayServer.ModelBinders
{
/// <summary>
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
/// <see cref="decimal"/>.
/// </summary>
public class InvariantDecimalModelBinder : IModelBinder
{
private readonly NumberStyles _supportedStyles;
public InvariantDecimalModelBinder()
{
_supportedStyles = NumberStyles.Any;
}
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var modelState = bindingContext.ModelState;
modelState.SetModelValue(modelName, valueProviderResult);
var metadata = bindingContext.ModelMetadata;
var type = metadata.UnderlyingOrModelType;
try
{
var value = valueProviderResult.FirstValue;
var culture = CultureInfo.InvariantCulture;
object model;
if (string.IsNullOrWhiteSpace(value))
{
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
model = null;
}
else if (type == typeof(decimal))
{
model = decimal.Parse(value, _supportedStyles, culture);
}
else
{
// unreachable
throw new NotSupportedException();
}
// When converting value, a null model may indicate a failed conversion for an otherwise required
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
// current bindingContext. If not, an error is logged.
if (model == null && !metadata.IsReferenceOrNullableType)
{
modelState.TryAddModelError(
modelName,
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
catch (Exception exception)
{
var isFormatException = exception is FormatException;
if (!isFormatException && exception.InnerException != null)
{
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
// this code in case a cursory review of the CoreFx code missed something.
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
}
modelState.TryAddModelError(modelName, exception, metadata);
// Conversion failed.
}
return Task.CompletedTask;
}
}
}

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)")]
@ -46,6 +52,7 @@ namespace BTCPayServer.Models.AppViewModels
public string TargetCurrency { get; set; } = "BTC";
[Display(Name = "Set a Target amount ")]
[Range(0, double.PositiveInfinity)]
public decimal? TargetAmount { get; set; }
@ -79,5 +86,11 @@ namespace BTCPayServer.Models.AppViewModels
public bool SortPerksByPopularity { get; set; }
[Display(Name = "Display contribution ranking")]
public bool DisplayPerksRanking { get; set; }
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
public string Sounds{ get; set; }
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
public string AnimationColors{ get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.AppViewModels
{
@ -10,7 +11,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 +25,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)]

View File

@ -1,13 +1,16 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Models.AppViewModels
{
public class ViewCrowdfundViewModel
{
public string HubPath { get; set; }
public string StatusMessage{ get; set; }
public string StoreId { get; set; }
public string AppId { get; set; }
@ -30,6 +33,8 @@ namespace BTCPayServer.Models.AppViewModels
public bool SoundsEnabled { get; set; }
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; }
public string[] AnimationColors { get; set; }
public string[] Sounds { get; set; }
public int ResetEveryAmount { get; set; }
public bool NeverReset { get; set; }
@ -50,11 +55,26 @@ namespace BTCPayServer.Models.AppViewModels
public DateTime? LastResetDate { get; set; }
public DateTime? NextResetDate { get; set; }
}
public class Contribution
{
public PaymentMethodId PaymentMehtodId { get; set; }
public decimal Value { get; set; }
public decimal CurrencyValue { get; set; }
}
public class Contributions : Dictionary<PaymentMethodId, Contribution>
{
public Contributions(IEnumerable<KeyValuePair<PaymentMethodId, Contribution>> collection) : base(collection)
{
TotalCurrency = Values.Select(v => v.CurrencyValue).Sum();
}
public decimal TotalCurrency { get; }
}
public bool Started => !StartDate.HasValue || DateTime.Now.ToUniversalTime() > StartDate;
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
public bool DisplayPerksRanking { get; set; }
public bool Enabled { get; set; }
}
public class ContributeToCrowdfund

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; }
@ -64,5 +65,7 @@ namespace BTCPayServer.Models.InvoicingModels
public bool CoinSwitchEnabled { get; set; }
public string CoinSwitchMode { get; set; }
public string CoinSwitchMerchantId { get; set; }
public string RootPath { get; set; }
public decimal CoinSwitchAmountMarkupPercentage { get; set; }
}
}

View File

@ -48,7 +48,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string Id { get; set; }
[Required] public string StoreId { get; set; }
[Required] public decimal Amount { get; set; }
[Required][Range(0, double.PositiveInfinity)]public decimal Amount { get; set; }
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]
public string Currency { get; set; }
@ -134,12 +134,14 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string AmountCollectedFormatted { get; set; }
public string AmountFormatted { get; set; }
public bool AnyPendingInvoice { get; set; }
public string HubPath { get; set; }
public class PaymentRequestInvoice
{
public string Id { get; set; }
public DateTime ExpiryDate { get; set; }
public decimal Amount { get; set; }
public string AmountFormatted { get; set; }
public string Status { get; set; }
public List<PaymentRequestInvoicePayment> Payments { get; set; }

View File

@ -18,8 +18,9 @@ namespace BTCPayServer.Models.ServerViewModels
{
get; set;
}
[EmailAddress]
[Display(Name = "Test Email")]
public string TestEmail
{
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

@ -25,19 +25,6 @@ namespace BTCPayServer.Models.StoreViewModels
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,23 @@ 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; }
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

@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.ModelBinders;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Models.StoreViewModels
{
public class PayButtonViewModel
{
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
public decimal Price { get; set; }
public string InvoiceId { get; set; }
[Required]

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

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

@ -12,6 +12,7 @@ using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.PaymentRequest
{
@ -59,6 +60,15 @@ namespace BTCPayServer.PaymentRequest
break;
}
}
public static string GetHubPath(HttpRequest request)
{
return request.GetRelativePathOrAbsolute("/payment-requests/hub");
}
public static void Register(HubRouteBuilder route)
{
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
}
}
public class PaymentRequestStreamer : EventHostedServiceBase

View File

@ -51,10 +51,8 @@ namespace BTCPayServer.PaymentRequest
{
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
var amountCollected =
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
if (amountCollected >= blob.Amount)
var contributions = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
if (contributions.TotalCurrency >= blob.Amount)
{
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
}
@ -80,17 +78,14 @@ namespace BTCPayServer.PaymentRequest
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
var amountCollected =
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
var amountDue = blob.Amount - amountCollected;
var paymentStats = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
var amountDue = blob.Amount - paymentStats.TotalCurrency;
return new ViewPaymentRequestViewModel(pr)
{
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
AmountCollected = amountCollected,
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
AmountCollected = paymentStats.TotalCurrency,
AmountCollectedFormatted = _currencies.FormatCurrency(paymentStats.TotalCurrency, blob.Currency),
AmountDue = amountDue,
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
@ -100,6 +95,7 @@ namespace BTCPayServer.PaymentRequest
{
Id = entity.Id,
Amount = entity.ProductInformation.Price,
AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency),
Currency = entity.ProductInformation.Currency,
ExpiryDate = entity.ExpirationTime.DateTime,
Status = entity.GetInvoiceState().ToString(),

View File

@ -19,11 +19,11 @@ namespace BTCPayServer.Payments.Changelly
private readonly bool _showFiat;
private readonly HttpClient _httpClient;
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
{
_apisecret = apiSecret;
_showFiat = showFiat;
_httpClient = httpClientFactory.CreateClient();
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri(apiUrl);
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
}

View File

@ -61,7 +61,7 @@ namespace BTCPayServer.Payments.Changelly
throw new ChangellyException("Changelly not enabled for this store");
}
var changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl, changellySettings.ShowFiat);
_clientCache.AddOrReplace(storeId, changelly);
return changelly;

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,15 +19,19 @@ namespace BTCPayServer.Payments.Lightning
public static int LIGHTNING_TIMEOUT = 5000;
NBXplorerDashboard _Dashboard;
private readonly SocketFactory _socketFactory;
public LightningLikePaymentHandler(
NBXplorerDashboard dashboard)
NBXplorerDashboard dashboard,
SocketFactory socketFactory)
{
_Dashboard = dashboard;
_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);
@ -63,7 +69,7 @@ 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");
@ -84,8 +90,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 +102,7 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
}
return info.NodeInfo;
return nodeInfo;
}
}
@ -104,22 +110,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,144 @@ 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;
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;
Channel<string> _CheckInvoices = Channel.CreateUnbounded<string>();
Task _CheckingInvoice;
Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>();
public LightningListener(EventAggregator aggregator,
InvoiceRepository invoiceRepository,
IMemoryCache memoryCache,
BTCPayNetworkProvider networkProvider)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
_memoryCache = memoryCache;
_NetworkProvider = networkProvider;
}
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, 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 +166,190 @@ 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;
public LightningInstanceListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
LightningSupportedPaymentMethod supportedPaymentMethod,
BTCPayNetwork network)
{
this.supportedPaymentMethod = supportedPaymentMethod;
this.invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
this.network = network;
}
internal bool AddListenedInvoice(ListenedInvoice invoice)
{
return _ListenedInvoices.TryAdd(invoice.PaymentMethodDetails.InvoiceId, invoice);
}
internal async Task<LightningInvoiceStatus?> PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation)
{
var client = supportedPaymentMethod.CreateClient(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 = supportedPaymentMethod.CreateClient(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

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

@ -51,6 +51,7 @@ namespace BTCPayServer
.ConfigureLogging(l =>
{
l.AddFilter("Microsoft", LogLevel.Error);
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
l.AddProvider(new CustomConsoleLogProvider(processor));

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

View File

@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
@ -40,7 +41,7 @@ namespace BTCPayServer.Services.Apps
{
var result =
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted);
switch (result)
{
case OkObjectResult okObjectResult:
@ -62,5 +63,13 @@ namespace BTCPayServer.Services.Apps
}
public static string GetHubPath(HttpRequest request)
{
return request.GetRelativePathOrAbsolute("/apps/hub");
}
public static void Register(HubRouteBuilder route)
{
route.MapHub<AppHub>("/apps/hub");
}
}
}

View File

@ -25,6 +25,7 @@ using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
using static BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel;
namespace BTCPayServer.Services.Apps
{
@ -33,7 +34,6 @@ namespace BTCPayServer.Services.Apps
ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
CurrencyNameTable _Currencies;
private readonly RateFetcher _RateFetcher;
private readonly HtmlSanitizer _HtmlSanitizer;
private readonly BTCPayNetworkProvider _Networks;
public CurrencyNameTable Currencies => _Currencies;
@ -41,13 +41,11 @@ namespace BTCPayServer.Services.Apps
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networks,
CurrencyNameTable currencies,
RateFetcher rateFetcher,
HtmlSanitizer htmlSanitizer)
{
_ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository;
_Currencies = currencies;
_RateFetcher = rateFetcher;
_HtmlSanitizer = htmlSanitizer;
_Networks = networks;
}
@ -94,17 +92,8 @@ namespace BTCPayServer.Services.Apps
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed).ToArray();
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed)).ToArray();
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.EnforceTargetAmount);
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.EnforceTargetAmount);
var currentAmount = await GetCurrentContributionAmount(
paymentStats,
settings.TargetCurrency, rateRules);
var currentPendingAmount = await GetCurrentContributionAmount(
pendingPaymentStats,
settings.TargetCurrency, rateRules);
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
var perkCount = invoices
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
@ -140,6 +129,7 @@ namespace BTCPayServer.Services.Apps
EnforceTargetAmount = settings.EnforceTargetAmount,
StatusMessage = statusMessage,
Perks = perks,
Enabled = settings.Enabled,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
@ -148,19 +138,21 @@ namespace BTCPayServer.Services.Apps
DisplayPerksRanking = settings.DisplayPerksRanking,
PerkCount = perkCount,
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
Sounds = settings.Sounds,
AnimationColors = settings.AnimationColors,
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
{
TotalContributors = invoices.Length,
CurrentPendingAmount = currentPendingAmount,
CurrentAmount = currentAmount,
ProgressPercentage = (currentAmount / settings.TargetAmount) * 100,
PendingProgressPercentage = (currentPendingAmount / settings.TargetAmount) * 100,
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
LastUpdated = DateTime.Now,
PaymentStats = paymentStats,
PendingPaymentStats = pendingPaymentStats,
PaymentStats = currentPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
PendingPaymentStats = pendingPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
LastResetDate = lastResetDate,
NextResetDate = nextResetDate
NextResetDate = nextResetDate,
CurrentPendingAmount = pendingPayments.TotalCurrency,
CurrentAmount = currentPayments.TotalCurrency
}
};
}
@ -288,46 +280,20 @@ namespace BTCPayServer.Services.Apps
.ToArray();
}
public async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
public Contributions GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
{
var result = new List<decimal>();
var ratesTask = _RateFetcher.FetchRates(
stats.Keys
.Select((x) => new CurrencyPair(primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
.Distinct()
.ToHashSet(),
rateRules).Select(async rateTask =>
{
var (key, value) = rateTask;
var tResult = await value;
var rate = tResult.BidAsk?.Bid;
if (rate == null)
return;
foreach (var stat in stats)
{
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, key.Right,
StringComparison.InvariantCultureIgnoreCase))
{
result.Add((1m / rate.Value) * stat.Value);
}
}
});
await Task.WhenAll(ratesTask);
return result.Sum();
}
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool softcap)
{
return invoices
var contributions = invoices
.Where(p => p.ProductInformation.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
.SelectMany(p =>
{
var contribution = new Contribution();
contribution.PaymentMehtodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike);
contribution.CurrencyValue = p.ProductInformation.Price;
contribution.Value = contribution.CurrencyValue;
// For hardcap, we count newly created invoices as part of the contributions
if (!softcap && p.Status == InvoiceStatus.New)
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
return new[] { contribution };
// If the user get a donation via other mean, he can register an invoice manually for such amount
// then mark the invoice as complete
@ -335,20 +301,38 @@ namespace BTCPayServer.Services.Apps
if (payments.Count == 0 &&
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Complete)
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
return new[] { contribution };
contribution.CurrencyValue = 0m;
contribution.Value = 0m;
// If an invoice has been marked invalid, remove the contribution
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Invalid)
return new[] { (Key: p.ProductInformation.Currency, Value: 0m) };
return new[] { contribution };
// Else, we just sum the payments
return payments
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
.Select(pay =>
{
var paymentMethodContribution = new Contribution();
paymentMethodContribution.PaymentMehtodId = pay.GetPaymentMethodId();
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId, _Networks).Rate;
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
return paymentMethodContribution;
})
.ToArray();
})
.GroupBy(p => p.Key)
.ToDictionary(p => p.Key, p => p.Select(v => v.Value).Sum());
.GroupBy(p => p.PaymentMehtodId)
.ToDictionary(p => p.Key, p => new Contribution()
{
PaymentMehtodId = p.Key,
Value = p.Select(v => v.Value).Sum(),
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
});
return new Contributions(contributions);
}
private class PosHolder

View File

@ -34,6 +34,56 @@ namespace BTCPayServer.Services.Apps
public bool UseAllStoreInvoices { get; set; }
public bool DisplayPerksRanking { get; set; }
public bool SortPerksByPopularity { get; set; }
public string[] AnimationColors { get; set; } = new string[]
{
"#FF6138", "#FFBE53", "#2980B9", "#282741"
};
public string[] Sounds { get; set; } = new string[]
{
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/dominating.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/doublekill.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/doublekill2.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/eagleeye.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood2.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood3.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/flawless.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/godlike.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/hattrick.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headhunter.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot2.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot3.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/holyshit.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/killingspree.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife2.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife3.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ludicrouskill.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/megakill.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/monsterkill.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/multikill.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/nade.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ownage.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/payback.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare2.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare3.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare4.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/rampage.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide2.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide3.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide4.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/teamkiller.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/triplekill.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ultrakill.wav",
"//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

@ -13,11 +13,11 @@ namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
IHttpContextAccessor httpContext;
TorServices torServices;
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext, TorServices torServices)
{
ExpectedHost = httpContext.HttpContext.Request.Host.Value;
ExpectedDomain = httpContext.HttpContext.Request.Host.Host;
ExpectedProtocol = httpContext.HttpContext.Request.Scheme;
this.httpContext = httpContext;
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG
Build = "Debug";
@ -26,15 +26,18 @@ namespace BTCPayServer.Services
#endif
Environment = env;
NetworkType = provider.NetworkType;
this.torServices = torServices;
}
public IHostingEnvironment Environment
{
get; set;
}
public string ExpectedDomain { get; set; }
public string ExpectedHost { get; set; }
public string ExpectedProtocol { get; set; }
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
@ -53,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

@ -717,6 +717,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

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

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

@ -35,6 +35,7 @@ namespace BTCPayServer.Services.Mails
get; set;
}
[Display(Name = "Enable SSL")]
public bool EnableSSL
{
get; set;

View File

@ -18,5 +18,9 @@ namespace BTCPayServer.Services
[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; }
}
}

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Rating;
using System.Threading;
namespace BTCPayServer.Services.Rates
{
@ -93,13 +94,13 @@ namespace BTCPayServer.Services.Rates
public bool DoNotAutoFetchIfExpired { get; set; }
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
public async Task<LatestFetch> UpdateIfNecessary()
public async Task<LatestFetch> UpdateIfNecessary(CancellationToken cancellationToken)
{
if (NextUpdate <= DateTimeOffset.UtcNow)
{
try
{
await Fetch();
await Fetch(cancellationToken);
}
catch { } // Exception is inside _Latest
return _Latest;
@ -108,7 +109,7 @@ namespace BTCPayServer.Services.Rates
}
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var latest = _Latest;
if (!DoNotAutoFetchIfExpired && latest != null && latest.Expiration <= DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1.0))
@ -116,7 +117,7 @@ namespace BTCPayServer.Services.Rates
Logs.PayServer.LogWarning($"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this.");
latest = null;
}
return (latest ?? (await Fetch())).GetResult();
return (latest ?? (await Fetch(cancellationToken))).GetResult();
}
private string GetExchangeName()
@ -126,14 +127,14 @@ namespace BTCPayServer.Services.Rates
return "???";
}
private async Task<LatestFetch> Fetch()
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken)
{
var previous = _Latest;
var fetch = new LatestFetch();
fetch.ExchangeName = GetExchangeName();
try
{
var rates = await _Inner.GetRatesAsync();
var rates = await _Inner.GetRatesAsync(cancellationToken);
fetch.Latest = rates;
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
fetch.NextRefresh = DateTimeOffset.UtcNow + RefreshRate;

View File

@ -0,0 +1,32 @@
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), new BidAsk(p.Value["buy"].Value<decimal>(), p.Value["sell"].Value<decimal>())))
.ToArray());
}
}
}

View File

@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using BTCPayServer.Rating;
using System.Threading;
namespace BTCPayServer.Services.Rates
{
@ -22,7 +23,7 @@ namespace BTCPayServer.Services.Rates
public string ExchangeName => BitpayName;
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
.AllRates

View File

@ -0,0 +1,29 @@
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 ByllsRateProvider : IRateProvider, IHasExchangeName
{
private readonly HttpClient _httpClient;
public ByllsRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => "bylls";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["public_price"]["to_price"].Value<decimal>();
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) });
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
@ -40,12 +41,12 @@ namespace BTCPayServer.Services.Rates
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
public Task<ExchangeRates> GetRatesAsync()
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
return _Inner.GetRatesAsync(cancellationToken);
});
}
}

View File

@ -11,6 +11,7 @@ using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using BTCPayServer.Rating;
using System.Threading;
namespace BTCPayServer.Services.Rates
{
@ -65,7 +66,7 @@ namespace BTCPayServer.Services.Rates
}
set
{
_LocalClient = null;
_LocalClient = value;
}
}
HttpClient _LocalClient;
@ -111,7 +112,7 @@ namespace BTCPayServer.Services.Rates
}
}
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await HttpClient.SendAsync(request);
var resp = await HttpClient.SendAsync(request, cancellationToken);
using (resp)
{

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

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -31,14 +32,16 @@ namespace BTCPayServer.Services.Rates
public string ExchangeName => _ExchangeName;
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
await new SynchronizationContextRemover();
var rates = await _ExchangeAPI.GetTickersAsync();
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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
@ -16,13 +17,17 @@ namespace BTCPayServer.Services.Rates
_Providers = providers;
}
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
foreach (var p in _Providers)
{
try
{
return await p.GetRatesAsync().ConfigureAwait(false);
return await p.GetRatesAsync(cancellationToken).ConfigureAwait(false);
}
catch when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch(Exception ex) { Exceptions.Add(ex); }
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
@ -8,6 +9,6 @@ namespace BTCPayServer.Services.Rates
{
public interface IRateProvider
{
Task<ExchangeRates> GetRatesAsync();
Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken);
}
}

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -28,7 +29,7 @@ namespace BTCPayServer.Services.Rates
}
set
{
_LocalClient = null;
_LocalClient = value;
}
}
@ -38,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;
@ -55,7 +86,7 @@ namespace BTCPayServer.Services.Rates
{ "ZCAD", "CAD" },
};
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var result = new ExchangeRates();
var symbols = await GetSymbolsAsync();
@ -135,7 +166,7 @@ namespace BTCPayServer.Services.Rates
}
}
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null)
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null, CancellationToken cancellationToken = default)
{
StringBuilder sb = new StringBuilder();
sb.Append("https://api.kraken.com");
@ -147,7 +178,7 @@ namespace BTCPayServer.Services.Rates
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
}
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
var response = await HttpClient.SendAsync(request);
var response = await HttpClient.SendAsync(request, cancellationToken);
string stringResult = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<T>(stringResult);
if (result is JToken json)

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
@ -20,7 +21,7 @@ namespace BTCPayServer.Services.Rates
return _Instance;
}
}
public Task<ExchangeRates> GetRatesAsync()
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new ExchangeRates());
}

View File

@ -3,6 +3,7 @@ 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;
@ -31,9 +32,9 @@ namespace BTCPayServer.Services.Rates
return true;
}
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all", cancellationToken);
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());

View File

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -38,12 +39,12 @@ namespace BTCPayServer.Services.Rates
public RateProviderFactory RateProviderFactory => _rateProviderFactory;
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules, CancellationToken cancellationToken)
{
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules, cancellationToken).First().Value;
}
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules, CancellationToken cancellationToken)
{
if (rules == null)
throw new ArgumentNullException(nameof(rules));
@ -59,7 +60,7 @@ namespace BTCPayServer.Services.Rates
{
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
{
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange);
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, cancellationToken);
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
}
dependentQueries.Add(fetching);

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