Compare commits

..

597 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
c0da81557b Fix missing authenticated URI for charge 2019-03-01 15:38:11 +09:00
81945c0737 Fix bug on spark external config parsing 2019-03-01 15:34:30 +09:00
9664e3d6a1 bump version and deps 2019-03-01 14:55:53 +09:00
0a8bd38e76 Danish support 2019-03-01 14:52:45 +09:00
013054fb82 Small refactor 2019-03-01 14:46:32 +09:00
d898f716d1 Add some tests on externalConnectionString 2019-03-01 14:33:32 +09:00
1d3f144d21 Support relative path for external services, simplify the code in Services 2019-03-01 13:20:21 +09:00
de29d87487 Fix QR code not showing full uri if using relative path 2019-02-28 23:01:25 +09:00
ebef085a9c Support relative path for Spark and RTL external url, check in server settings if we are using a secure protocol 2019-02-28 22:20:14 +09:00
2c1f159d72 Document error of reverse proxy configuration 2019-02-28 21:43:44 +09:00
1ef59e05a5 fix dockerfile 2019-02-27 21:58:28 +09:00
5e09992637 Fix dockerfile 2019-02-27 21:56:36 +09:00
30448233b1 Fix dockerfile 2019-02-27 21:49:25 +09:00
a1601a17aa Fix permission 2019-02-27 21:44:16 +09:00
7522f7d0f7 fix remaining edgecase with payment request pay endpoint (#619) 2019-02-27 21:42:25 +09:00
d04b9c4c09 Remove last reference to externalurl 2019-02-27 21:41:02 +09:00
1a24ff9a49 bump 2019-02-27 21:41:01 +09:00
13d72de82d fix payment request redirect url (#617) 2019-02-27 20:25:13 +09:00
3728fdab3f improve warning message 2019-02-27 18:54:19 +09:00
2317e3d50c Make sure we rewrite the request scheme 2019-02-27 18:52:11 +09:00
5f15976c02 bump 2019-02-27 18:46:15 +09:00
7f592639c5 Remove URI rewritting and ExternalUri stuff 2019-02-27 18:38:11 +09:00
a98402af12 Making currency switching indicator more obvious with button style (#616) 2019-02-27 13:45:58 +09:00
316ffa91d1 bump 2019-02-26 23:10:58 +09:00
c24953b57e Making hamburger light to see it on dark background (#613) 2019-02-26 14:08:03 +09:00
7a1b1b7e5e Merge branch 'payment-requests' 2019-02-25 17:59:02 +09:00
70f71f64c4 Use internal tags, not order id in the streamer to know if the incoming invoice is for the payment request 2019-02-25 17:56:29 +09:00
5bccd07d7d Make sure the invoiceEvent is from a payment request. 2019-02-25 17:56:29 +09:00
d818baa6d1 Fix crowdfund test 2019-02-25 17:56:29 +09:00
249b8abf03 deduct network from contributions + removed unsued Enabled properties 2019-02-25 17:56:29 +09:00
c134277514 remove creating state from payment requests 2019-02-25 17:56:29 +09:00
f5d366cf7f Fix final bugs 2019-02-25 17:56:29 +09:00
ad25a2ed08 Add payment requests 2019-02-25 17:56:28 +09:00
1e7a2ffe97 Enable/Disable tips and discount. Fix custom amount. (#612) 2019-02-25 15:11:03 +09:00
dd52075ff1 Add backoff delay if fetching exchange rate is failing 2019-02-24 22:00:30 +09:00
0253e42bd5 Do not poll in the invoice page if websocket are working 2019-02-23 15:22:17 +09:00
d99774f8d9 fix tests 2019-02-22 22:52:43 +09:00
d563a2ec89 Fix tests 2019-02-22 22:48:39 +09:00
b4b4523193 Round currency up to significant decimal 2019-02-22 22:15:25 +09:00
fbcb69f447 Do not prevent btcpayserver from starting if using insecure protocol for lightning services 2019-02-22 18:24:27 +09:00
8ae5a9c1f7 Fix old crowdfunding invoices 2019-02-22 17:51:38 +09:00
3ef5bfb6eb bump 2019-02-22 17:30:54 +09:00
4016ded584 Affect orderId to crowdfund app invoices 2019-02-22 17:29:54 +09:00
5b0b4adb1c Fix service link for RTL 2019-02-22 15:46:43 +09:00
f1ec3b0c75 bump 2019-02-22 15:08:45 +09:00
b5d55a2066 Add RTL support 2019-02-22 15:06:52 +09:00
0d2c9fe377 Fix https://github.com/btcpayserver/btcpayserver/issues/585 2019-02-22 13:52:35 +09:00
2c7cc9a796 Fix: invoice Price was not being rounded if no taxIncluded present 2019-02-21 21:58:49 +09:00
2e1d623755 fix https://github.com/btcpayserver/btcpayserver/issues/596 2019-02-21 21:30:30 +09:00
52fee8f842 Make sure no nullreferenceexception is thrown if invalid invoice 2019-02-21 19:36:05 +09:00
6ba17e8e30 Can filter supported payment methods for an invoice 2019-02-21 19:34:11 +09:00
ac3432920a Fix build 2019-02-21 18:42:12 +09:00
63c88be533 Use CreateInvoiceRequest instead of NBitpay Invoice type 2019-02-21 18:40:27 +09:00
3cb577e6ba Add link back to official website 2019-02-21 14:04:03 +09:00
1e0d64c548 Improve homepage, document mattermost and point on the official website. 2019-02-21 13:50:46 +09:00
bc1b9ff59c update translations 2019-02-20 23:16:13 +09:00
7d73bed3be bump 2019-02-20 23:06:52 +09:00
126fbdfd60 Fix null reference exception if the NotificationUrl is not set 2019-02-20 23:03:04 +09:00
15094436fd bump lnd 2019-02-20 21:29:16 +09:00
010c653995 Create EventHostedServiceBase and make AppHubStreamer use this 2019-02-20 12:27:10 +09:00
119f82fd4e Properly aggregate contributions amount 2019-02-19 16:15:14 +09:00
3bbf4de5d2 Fix live update of crowdfunding, add tests, consider payments as confirmed if invoice is confirmed 2019-02-19 16:01:28 +09:00
0807f3b87b Remote internal tags at store level 2019-02-19 13:24:04 +09:00
4e9b3b40aa Fix crowdfunding-admin js file not being included 2019-02-19 13:20:06 +09:00
cc444811db Rename CrowdfundHubStream to AppHubSteamer 2019-02-19 13:18:30 +09:00
50c8525012 Moving CrowdfundSettings in its own file 2019-02-19 13:07:10 +09:00
aedad497e8 Rename AppsHelper to AppService 2019-02-19 13:04:58 +09:00
b1b231e645 Add tests on tagging 2019-02-19 12:59:12 +09:00
dc46fd225a Migrate old crowdfund deployment to the new tagging system 2019-02-19 12:53:24 +09:00
6226de7cff Refactor Crowdfund to use the tagging system 2019-02-19 12:48:48 +09:00
37327ec674 Apps can tag invoices 2019-02-19 12:48:08 +09:00
c071c81403 Pass the whole Entity object to internal InvoiceEvent 2019-02-19 12:08:07 +09:00
85d75a013a The invoices link of crowdfund show all invoices of the store if it is set to use all store's invoice 2019-02-19 11:45:04 +09:00
3816b36131 Add internal tags to invoice 2019-02-19 11:14:21 +09:00
dc7965267b Use GetRelativePathOrAbsolute in ViewCrowdfund and ViewPointOfSale 2019-02-19 00:28:44 +09:00
ce9a6bced7 Use GetRelativePathOrAbsolute in ShowLightningNodeInfo 2019-02-18 12:25:14 +09:00
85325dc710 Update translations 2019-02-18 12:24:55 +09:00
ac4050df70 Improve the UI of lightning node info 2019-02-17 19:40:39 +09:00
a16a53167b Can put lightning node info inside an XFrame 2019-02-17 19:30:16 +09:00
afab3cf847 Better Datetime picker picker in crowdfund page 2019-02-17 19:25:18 +09:00
8fdaeb7bac Fix race condition on calculation of contributions, refactor the methods to AppHelper 2019-02-17 19:17:59 +09:00
7e0f9f6e0d Inject HtmlSanitizer in AddBTCPayServer, remove AppHelpers deps when possible 2019-02-17 18:47:25 +09:00
5b1bf6cd88 add email to export (#583) 2019-02-17 18:33:40 +09:00
b1584c352b Free some memory 2019-02-17 16:13:16 +09:00
b06b83503c Better status message 2019-02-15 10:05:29 -06:00
b03d89c190 Different message for admin deletion, check not to delete last admin
Ref: #549, #550
2019-02-15 10:05:29 -06:00
f53548d10f Showing warning when user tries to delete last admin 2019-02-15 10:05:29 -06:00
5ec2f54d7f Merge pull request #593 from BenSanex/bugfix/591_FixValidationMessage
Custom validation message for Crowdfund form primary currency
2019-02-15 10:03:05 -06:00
db588ff961 I've added asterisk. Isn't that impressive? 2019-02-15 10:02:17 -06:00
2b7006a14c add asterisk, revert primary currency error message, remove the 2019-02-11 21:53:45 -06:00
8f5f07882f Custom validation message for Crowdfund form primary currency 2019-02-07 20:27:26 -06:00
0eee8e7464 Returns Access-Control-Allow-Origin * on all Bitpay GET and post requests. 2019-02-02 16:12:51 +09:00
3725a5b644 Correctly set Access-Control-Allow-Headers 2019-02-02 15:51:38 +09:00
c84c0ac64d set CORS headers 2019-02-02 15:22:00 +09:00
098e07988c Bypass MVC for replying to CORS requests if Bitpay API 2019-02-02 15:19:22 +09:00
66bb702aca Fix CORS for bitpay API again 2019-02-02 13:58:32 +09:00
03ff2fedf0 Update Translator grammar (#579) 2019-02-01 17:35:49 +09:00
c707f47b11 bump 2019-01-31 22:03:46 +09:00
585efa3ff5 Fix: Default payment method should not return a disabled one 2019-01-31 22:03:28 +09:00
07d0b98a23 Update language 2019-01-31 19:33:07 +09:00
c7c0f01010 bump 2019-01-31 19:24:36 +09:00
cf6b17250a Can set lightning network as default payment method (close #290) 2019-01-31 19:07:38 +09:00
90503a490c Add dots to make derivation examples clearer (#561) 2019-01-31 17:00:15 +09:00
ebdd53b99b fix unfairly long dropdown in ledger account selection (#574)
Closes #570
2019-01-31 16:56:39 +09:00
51a5d2e812 Refactor XFrames Attribute & simplify pos settings page (#576)
* Enable better error when invoice cannot be created on crowdfund

Closes #572

* Allow all public apps in iframe

* cleanup pos page dev info
2019-01-31 16:56:21 +09:00
2ad509d56a Update Readme.md (#577)
* Update readme

* Update README.md

* add apps link

* fix broken link
2019-01-31 16:55:27 +09:00
1a98bfba36 Fix formatting of currencies in Invoice detail page 2019-01-30 19:18:44 +09:00
d05bb6c60e Properly format currencies in Invoice list 2019-01-30 19:01:18 +09:00
ed81b6a6aa bump 2019-01-30 15:52:31 +09:00
264914588f fix bitpay API not having CORS 2019-01-30 14:57:10 +09:00
05df43b426 fix bitpay API not having CORS 2019-01-30 14:36:26 +09:00
0334a4e176 bump 2019-01-30 13:46:55 +09:00
38dca425da Fix repetitive IPN for lightning network payments (https://github.com/btcpayserver/btcpayserver/issues/564) 2019-01-30 13:40:08 +09:00
82d4a79dd4 Fix potential crash if the current host is an IP instead of DNS name, might fix https://github.com/btcpayserver/btcpayserver/issues/543 2019-01-30 12:52:34 +09:00
6725be8145 Remove warnings 2019-01-29 18:35:27 +09:00
f5b693f01b Disable quadricagx tests because exchange is down 2019-01-29 18:34:30 +09:00
f09f23e570 Enable better error when invoice cannot be created on crowdfund (#575)
Closes #572
2019-01-29 18:32:44 +09:00
4f4d05b8cd Make sure CORS is enabled on Bitpay's API 2019-01-29 18:20:53 +09:00
0c5b5ff49c Add link to no stores error (#558)
* add link

* safer status message

* refactor

* small view cleanup
2019-01-29 16:44:46 +09:00
a815fad3f1 Put back the list of ledger accounts to 5. 2019-01-29 13:06:43 +09:00
d8b1c7c10a Fix broken lightning payments on Checkout page 2019-01-28 18:50:26 +09:00
02e1aea80c add warning for third parties (#562)
* add warning for third parties

* Update UpdateCoinSwitchSettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateCoinSwitchSettings.cshtml
2019-01-28 17:40:23 +09:00
1892f7e0f4 rename field 2019-01-28 17:10:51 +09:00
b7b50349a7 Convert Ledger account list to dropdown and add more accounts to list (#560) 2019-01-28 17:07:01 +09:00
02d227ee02 Fix connection to checkout backend (bad links) 2019-01-28 16:24:11 +09:00
47f8938b89 Catch websocket connection issues 2019-01-28 15:12:40 +09:00
4945a640a7 Use PaymentHash of a lightning payment as PaymentId 2019-01-27 13:06:55 +09:00
0136977359 update translations 2019-01-26 21:23:41 +09:00
0acd3e20b0 bump 2019-01-26 20:58:15 +09:00
30bdfeee37 Enhance PosData Viewer & add cart to posdata in POS app (#559) 2019-01-26 13:26:49 +09:00
7ea665d884 Merge pull request #557 from Kukks/master
Fix close invoice button for modal invoices #555
2019-01-25 20:48:45 +09:00
073edcfb12 Merge remote-tracking branch 'btcpayserver/master' 2019-01-25 12:41:20 +01:00
a645366a25 Fix close invoice button for modal invoices #555 2019-01-25 12:41:15 +01:00
12aa0b7abd Merge pull request #556 from ChekaZ/master
Support Bitcoinplus
2019-01-25 16:09:39 +09:00
3f98a50410 Support Bitcoinplus 2019-01-25 01:03:04 +01:00
24c8c076d5 Add taxIncluded field in invoice 2019-01-24 20:53:29 +09:00
37e6931d33 Improve help 2019-01-23 17:44:03 +09:00
86493568e9 Fix external services parsing 2019-01-23 13:31:00 +09:00
bb51436ae3 Accept absolute url for external services 2019-01-23 13:17:36 +09:00
854a55ac1a Merge branch 'store-level-email' 2019-01-22 21:39:55 +09:00
cfb4b080d3 Emails on store level 2019-01-22 21:38:39 +09:00
00aa2e4e17 Merge pull request #546 from britttttk/fix/message
Fix delete user message
2019-01-21 17:11:24 +09:00
69c67d99f6 Fix message for delete user 2019-01-20 21:19:01 -07:00
65596ec8c1 fix delete user message 2019-01-20 21:12:20 -07:00
49643cb00e Make CanScheduleBackgroundTasks more robust 2019-01-19 21:19:15 +09:00
35b0faee57 Merge pull request #541 from Horndev/patch-2
Improve exception messages in server configuration parsing.
2019-01-19 20:49:08 +09:00
88ef4d69b2 Improve help and exception messages.
Improve the messages passed to users and in exceptions when parsing the BTCPayServerOptions configuration.
2019-01-18 10:23:47 -04:00
575b6ca222 Improve error messages when the store has no payment method configured 2019-01-18 19:15:31 +09:00
b5a0e844d2 Cann GetInvoicesTotal in parallel 2019-01-17 23:40:47 +09:00
2642e11ce2 Fix amount format in wallet send 2019-01-17 23:37:39 +09:00
b4fe655efe Merge pull request #534 from sipsorcery/fixpaging
Small improvement to the paging buttons on the list invoices page
2019-01-17 11:14:31 +09:00
ffb761909a Merge pull request #535 from dalijolijo/master
Change default exchange for Bitcore
2019-01-17 11:10:55 +09:00
b443e1ac6e Change default exchange for Bitcore 2019-01-16 22:04:24 +00:00
a4792f54a7 Added bootstrap paging buttons to the invoice list page and fixed paging buttons. 2019-01-16 21:33:04 +01:00
686ae029e0 bump 2019-01-16 23:49:40 +09:00
f3fd2e7d0f Update translations 2019-01-16 23:08:48 +09:00
7efd9ba0a5 Fix ledger on firefox 2019-01-16 23:07:22 +09:00
1c2a6bb8a1 Delete unused code 2019-01-16 19:35:29 +09:00
7bcf1cbdd5 Remove references to hangfire 2019-01-16 19:30:03 +09:00
2aaa2544bd Do not send mail synchronously in InvoiceNotificationManager 2019-01-16 19:21:02 +09:00
d85f03ba20 Remove HangFire dependency 2019-01-16 19:15:09 +09:00
cfb51a6be4 Merge pull request #531 from Kukks/patch-2
fix merge bug
2019-01-16 15:58:29 +09:00
c9d778c94b Bump nbitcoin and nbxplorer 2019-01-16 15:16:41 +09:00
fd62f882de fix merge bug 2019-01-15 18:18:41 +01:00
adc050f190 Trim destination address 2019-01-16 01:19:37 +09:00
2d551b9fc5 bump 2019-01-16 00:13:03 +09:00
884acdde32 Disabled POLIS and Bitcoin because default exchange (cryptopedia) is down 2019-01-16 00:08:17 +09:00
8f896de794 Merge pull request #516 from Kukks/feature/crowdfund
Crowdfund Bug fixes
2019-01-15 23:57:29 +09:00
5e4e26d2fd Merge pull request #529 from Kukks/bugfix/app-proper-redirect
fix app redirect to app instead of root url
2019-01-15 23:54:54 +09:00
ae688e6615 Merge pull request #530 from Horndev/patch-1
Grammatical corrections in exception messages
2019-01-15 23:54:20 +09:00
c4c812bdf6 Remove cryptopia from directly queried exchanges 2019-01-15 23:53:32 +09:00
e620fc0283 Add expert mode to BTCPay with No Change UTXO option 2019-01-15 23:50:45 +09:00
c333902468 Round up invoice price 2019-01-15 22:12:29 +09:00
4c83ecd06a Remove unused code 2019-01-15 21:56:33 +09:00
b28a547dc4 Grammatical corrections in exception messages
Fixed a few grammatical errors in LightningLikePaymentHandler.
2019-01-15 12:21:31 +00:00
6bc17e05bd add ids for better styling possibilities 2019-01-15 13:12:19 +01:00
0903350d30 add more log 2019-01-15 12:12:17 +01:00
6c0f19b457 Merge branch 'master' into feature/crowdfund 2019-01-15 09:48:16 +01:00
e119dc823f fix app redirect to app instead of root url 2019-01-15 09:46:07 +01:00
43295c9c57 Merge pull request #528 from bolatovumar/master
Update .NET Core SDK version in documentation
2019-01-15 16:42:40 +09:00
ded8b54042 Merge pull request #524 from Kukks/escapedstorename
Fix Store name character escaping on paid invoice #522
2019-01-15 16:41:51 +09:00
50a3178d51 Update .NET Core SDK version in documentation
Address #523
2019-01-14 16:49:12 -08:00
393c226032 fix escaped store name in return 2019-01-14 09:32:22 +01:00
f2630df387 dispose streamer properly 2019-01-14 08:21:27 +01:00
abcd2c1750 add padding when disqus enabled 2019-01-14 08:01:07 +01:00
cc95f3b5b5 fix exponent numbers in contribution amounts 2019-01-14 07:53:03 +01:00
a08ee93b43 fix issue with perk ordering 2019-01-14 07:45:21 +01:00
4b90f873d5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-11 10:52:31 +01:00
419ab8e0b1 add loader and fix perk badge zindex 2019-01-11 10:52:21 +01:00
c95ef27998 bump 2019-01-11 00:17:58 +09:00
63dfd93834 Merge pull request #511 from Kukks/feature/crowdfund
crowdfund: change text
2019-01-11 00:17:37 +09:00
57610881de change text 2019-01-10 16:12:00 +01:00
7469faf296 Merge pull request #510 from Kukks/feature/crowdfund
fix notif number
2019-01-10 23:58:57 +09:00
55a884a559 fix js number 2019-01-10 15:57:30 +01:00
ee2b3c3d10 bump 2019-01-10 23:41:08 +09:00
e5819a260b Merge pull request #509 from Kukks/feature/crowdfund
missed commit crowdfund
2019-01-10 23:40:52 +09:00
a3ecf48702 fix pos update too 2019-01-10 15:37:50 +01:00
1c0b904cd2 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-10 15:35:19 +01:00
072d8a1728 fix exponents in js product editor 2019-01-10 15:35:03 +01:00
964e541c32 Update translations 2019-01-10 23:33:12 +09:00
78fec4ed22 bump 2019-01-10 23:31:35 +09:00
ef111d36c9 Merge pull request #484 from Kukks/feature/crowdfund
New App: Crowdfunding 🎉 🎉
2019-01-10 23:30:31 +09:00
4f64193e85 add rank badge to minimal and fix css in minimal 2019-01-10 14:54:41 +01:00
89bb6d1268 add validation for ranking 2019-01-10 14:43:47 +01:00
9f4226bf0f remove inline styles and fix checkbox setting text 2019-01-10 14:19:06 +01:00
a87c2a3374 Merge remote-tracking branch 'origin/master' into feature/crowdfund 2019-01-10 09:50:22 +01:00
d7294ba5a0 fix product item template 2019-01-10 09:28:51 +01:00
82d286dc6f Fix test 2019-01-10 14:00:26 +09:00
1fa18ab997 Merge pull request #507 from hubiktomas/patch-1
Typo fix
2019-01-10 13:49:52 +09:00
afc90f32c9 Fix tests 2019-01-10 13:47:21 +09:00
e9cfb7c21e Update link to accounting doc 2019-01-10 13:08:25 +09:00
1af8ea3769 Typo fix 2019-01-09 17:35:32 +01:00
9f7af190f1 fix ranking style 2019-01-09 15:44:16 +01:00
9c703fe94d fix number issue 2019-01-09 12:55:02 +01:00
a7a11a4f13 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-09 12:22:42 +01:00
c32c3bb62b add contribution ranking 2019-01-09 12:22:36 +01:00
e29d1480a6 Add link to doc for export 2019-01-09 17:28:30 +09:00
8f299d7791 Fix build 2019-01-09 17:25:46 +09:00
65fb2e992e Round InvoiceDue and PaidCurrency in export 2019-01-09 17:18:01 +09:00
41f5d677d5 Merge pull request #491 from bitcoinshirt/bitcoinshirt-patch-ny
Update 2019 license
2019-01-09 13:33:56 +09:00
a2b78b8cd9 Merge pull request #506 from britttttk/fix/PasswordLength
Fix registration password length
2019-01-09 13:33:41 +09:00
c93f217033 Fix minimum registration password length 2019-01-08 18:32:07 -07:00
82c47b6e9a fix margin on crowdfund 2019-01-08 21:42:11 +01:00
94fb738c67 fix choice key and currency data 2019-01-08 15:49:07 +01:00
89071e40fc oops 2019-01-08 15:14:06 +01:00
95a90c410e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 15:10:13 +01:00
59fc371cd5 perk count + img fixer 2019-01-08 15:10:05 +01:00
9b404e330d bump 2019-01-08 23:03:53 +09:00
1667f9b2ef Merge pull request #504 from Kukks/bugfix/general
Fix Coinswitch Issues, Fix LN Node Info Clipboard, Fix Vue-Cloak Styles
2019-01-08 23:02:34 +09:00
caadfc8641 use bolt icon in view 2019-01-08 13:52:44 +01:00
bffc2e70c1 use summernote instead 2019-01-08 13:52:30 +01:00
8b686f0b12 fix coin switch issues 2019-01-08 11:27:37 +01:00
def8d1e0cb fix ln node clipboard 2019-01-08 10:54:02 +01:00
ca28c34be0 fix ln payment calculator 2019-01-08 10:32:10 +01:00
196bc3ea00 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 09:50:50 +01:00
b15267be4d Merge pull request #453 from 2pac1/master
Update Anyone can enable invoices text so its much more clear
2019-01-08 12:56:04 +09:00
5c074f6f5f Update translations 2019-01-07 22:51:26 +09:00
04cba61888 add bundle helper 2019-01-07 14:40:51 +01:00
a41e2e1ceb Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:39:08 +01:00
d1d03c98ba pr changes 2019-01-07 14:39:04 +01:00
679942159e bump 2019-01-07 22:37:55 +09:00
3e48a54ab5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:25:41 +01:00
f6e389ff62 fix issues 2019-01-07 14:25:35 +01:00
63c309bd12 Merge pull request #499 from Kukks/node-info-page
Add Node Info Page
2019-01-07 22:04:08 +09:00
561ec57cc8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 11:13:02 +01:00
3cefd7bd1e Merge pull request #467 from Kukks/feature/coinswitch
CoinSwitch Integration
2019-01-07 19:11:55 +09:00
c63feb488c Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 10:58:52 +01:00
12c418d84d Add Node Info Page 2019-01-07 09:52:27 +01:00
4b982f815c Renaming 2019-01-07 15:35:18 +09:00
d4d3346b6d Merge pull request #463 from sipsorcery/455-disablereg
Set disable registration as default true
2019-01-07 15:20:04 +09:00
6010a103e0 Added new disable-registration command line option. 2019-01-06 16:43:55 +01:00
5dc1da2af0 Don't disable user registrations if debug for unit tests. 2019-01-06 14:55:18 +01:00
f2ccc4d963 Add sanity check in loading crowdfun 2019-01-06 14:44:51 +01:00
a92d48efdd move button below help text 2019-01-06 14:37:40 +01:00
b633206b45 add helpful texts 2019-01-06 14:28:53 +01:00
b6f3d2af5e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 14:12:25 +01:00
5fd77d9fcc Merge pull request #492 from Kukks/crowdfund-part-1
Crowdfund Part 1: JS Dependencies
2019-01-06 21:59:52 +09:00
5ca4494eed reorder options in update crowdfund 2019-01-06 13:51:40 +01:00
de7e419ef4 fix overflow of descriptions 2019-01-06 13:50:30 +01:00
20a6b3fc33 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 10:25:24 +01:00
540414d8f5 Merge pull request #495 from Kukks/crowdfund-part-3
Enhance Invoice Events
2019-01-06 18:14:56 +09:00
abcdb8ced0 Merge pull request #494 from Kukks/crowdfund-part-2
Crowdfund Part 2: Expand Invoice Searching
2019-01-06 18:14:23 +09:00
5076d73695 Enhance Invoice Events 2019-01-06 10:12:45 +01:00
d88735f84e Merge remote-tracking branch 'upstream/master' into 455-disablereg 2019-01-06 10:05:33 +01:00
2244f0ab76 Merge pull request #493 from btcpayserver/feature/fastertests
[WIP] Make tests fast to execute
2019-01-06 18:04:38 +09:00
40c85d6104 Expand Invoice Searching 2019-01-06 10:00:55 +01:00
42892e24f4 Remove uneeded database call during derivation scheme registration 2019-01-06 17:58:11 +09:00
e6357d2ac8 fix build 2019-01-06 09:29:21 +01:00
1eecd85ceb Merge branch 'master' into feature/crowdfund 2019-01-06 09:26:58 +01:00
c27557826b add vendors 2019-01-06 09:08:05 +01:00
88150b6535 Improve IPN tests 2019-01-06 15:04:30 +09:00
d63176da19 Update BTCPayServer.csproj 2019-01-05 22:38:44 +01:00
887da5aa9a new year 2019-01-05 22:22:19 +01:00
6e7f1151bc bug fixes and optimizations 2019-01-05 19:47:39 +01:00
b2aebcc5d3 Merge pull request #480 from britttttk/fix/PaymentButton
Fix payment button size
2019-01-05 22:32:11 +09:00
ae9ad0fa65 Merge pull request #489 from btcpayserver/feature/networkfee
Add support for removing network fee on first payment
2019-01-05 22:09:10 +09:00
fb6d852827 switc back to regtest 2019-01-05 10:18:01 +01:00
ba17612461 Link to associated invoices 2019-01-05 10:17:52 +01:00
a15c7a0213 change crowdfund app prefix to not break invoice searcher 2019-01-05 09:53:57 +01:00
7e321d4016 expand list invoices search 2019-01-05 09:49:06 +01:00
a05cd5678b Add support for removing network fee on first payment 2019-01-05 17:45:49 +09:00
2ccf007b9a fix permissions 2019-01-05 09:38:27 +01:00
895b8c2c80 ux fixes 2019-01-05 09:18:15 +01:00
0f175174f6 Rename TxFee to NetworkFee and save the Network Fee of each payment under PaymentEntity 2019-01-05 13:31:05 +09:00
493466683c start adding UTs 2019-01-04 16:42:35 +01:00
761c342c51 add validation 2019-01-04 13:47:06 +01:00
5341da28d9 add date time picker 2019-01-04 12:58:29 +01:00
7768f41849 add reset every x amount of time feature 2019-01-04 11:42:37 +01:00
fa8993191e Update coinswitch.html 2019-01-03 15:31:56 +01:00
239ce28575 Update coinswitch.html 2019-01-02 21:50:43 +01:00
c52a49f747 add minimal crowdfund version 2019-01-02 14:08:30 +01:00
e4b9895ba7 add rich text and options 2019-01-02 12:47:06 +01:00
92a2bb4d32 fixes to computed goal result 2019-01-02 12:04:35 +01:00
bfec722312 protect contrib endpoint when needed 2019-01-02 11:29:47 +01:00
5a3f7b5b70 add in more info and simplify backend model 2019-01-02 10:41:54 +01:00
2aa097be46 fix cache expiration time 2019-01-02 09:45:04 +01:00
8a646d85c6 fix bundles 2019-01-02 09:03:20 +01:00
890b3eaa00 remove api key for disqus 2019-01-02 08:10:42 +01:00
cda28ebf15 ui fixes + toggle sound options 2018-12-31 13:20:00 +01:00
2245027ca3 ux fixes 2018-12-31 12:34:27 +01:00
3dc250f801 Add Disqus & fix ux 2018-12-31 11:38:05 +01:00
66e786a1b0 styles and perks 2018-12-30 20:28:36 +01:00
1e26926350 Fix payment button size 2018-12-30 00:27:22 -07:00
8bd7ea5bbc start ux for perks 2018-12-29 20:21:23 +01:00
6eb36abe2e start contrib perks 2018-12-29 11:52:07 +01:00
6fced3fab2 better tooltips and icons 2018-12-29 10:19:50 +01:00
774e456e54 cleanup 2018-12-28 23:57:39 +01:00
519859e1c5 ux fixwes 2018-12-28 23:31:41 +01:00
26f0c488e5 hook up proper payments to events and super crazy ux shit 2018-12-28 23:12:16 +01:00
1b0b53fbd0 small ux fixes 2018-12-28 18:23:32 +01:00
35f4ea29f9 more integration 2018-12-28 17:38:20 +01:00
fff39d9879 Merge remote-tracking branch 'origin/feature/coinswitch' into feature/coinswitch 2018-12-28 12:34:01 +01:00
444e761d41 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-28 12:33:55 +01:00
68a3def35a save state for coinswitch started order 2018-12-28 12:33:47 +01:00
d9426d301d Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-28 12:08:32 +01:00
8bcf7109a3 integrate invoice popup 2018-12-28 12:07:15 +01:00
3effdf0f4d Update UpdateCoinSwitchSettings.cshtml 2018-12-28 10:49:12 +01:00
3c122bcf53 Merge pull request #478 from mariodian/pos-fix-search-btn
PoS: Fix z-index of search cancel button that overlaps modal confirmation
2018-12-28 16:12:59 +09:00
037ff52f4f Fix z-index of search cancel button that overlaps modal confirmation 2018-12-28 11:13:04 +08:00
b11f8acba1 wip 2018-12-28 00:10:03 +01:00
ef9a633aa4 fixes for hub 2018-12-27 20:55:46 +01:00
c7e2f979dd Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-27 20:19:29 +01:00
e97bb9c933 work on vue and signalr fro crowdfund 2018-12-27 20:19:21 +01:00
fa506b5bf8 bump 2018-12-27 18:52:01 +09:00
e3193a92d0 Add PaidCurrency in the excel export 2018-12-27 16:48:33 +09:00
d76dabdca6 Remove warning 2018-12-27 16:19:00 +09:00
d219f50912 Merge branch 'pos-new-design' 2018-12-27 16:17:07 +09:00
e08710a19c Fix tests 2018-12-27 16:11:58 +09:00
4e167b35be Bug fixes
- fix `tip reset` when cart content changes
- fix negative cart value when deleting empty cart items
2018-12-27 13:19:51 +08:00
16873384a8 - fix cart item removal
- fix empty qty field
- remove tip when total changes
2018-12-27 12:57:31 +08:00
f87339f9fa Change CustomTipPercentages type to int[] 2018-12-27 12:57:31 +08:00
5f5e5e3211 Add missing AppId 2018-12-27 12:57:31 +08:00
f724db8226 Fix cart table widths 2018-12-27 12:57:31 +08:00
c7e90cd7df New PoS design 2018-12-27 12:57:31 +08:00
8c5b00b1a3 Merge pull request #470 from Kukks/feature/bootstrapbump
update bootstrap to 4.2.1
2018-12-27 13:40:42 +09:00
a3b79fbcd8 Merge pull request #462 from rockstardev/master
InvoiceDue field in export
2018-12-26 16:47:38 +09:00
9db77e6351 Rewrite and comment non obvious code for ledger 2018-12-26 15:10:00 +09:00
5bc1eaec9f bump 2018-12-26 15:04:43 +09:00
81c9ce7284 Limit the number of time the wallet need to export the xpub 2018-12-26 15:04:11 +09:00
caa6978d80 Save the KeyPath of the WalletKeyPathRoot of the hardware wallet so we don't have to scan for it 2018-12-26 14:04:00 +09:00
af22d6a4e3 Remove preliminary test to know if the ledger can handle the store. If it can't signing will fail anyway. 2018-12-25 19:33:03 +09:00
0eabb3c37c Remove useless query to ledger xpub in the Add derivation scheme screen 2018-12-25 19:02:11 +09:00
2b84791391 fix raw html 2018-12-22 21:03:43 +01:00
6f896cb096 update bootstrap to 4.2.1 2018-12-22 17:32:03 +01:00
9a488c60f2 fix some styling 2018-12-22 17:30:54 +01:00
9cb50446f4 update bootstrap to 4.2.1 2018-12-22 16:55:24 +01:00
8c00a2359e better layout 2018-12-22 15:43:40 +01:00
d1ff34d16d add minimal crowdfund system and UI 2018-12-22 15:02:16 +01:00
8e8615dab8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-21 11:51:13 +01:00
a63ed4d3b4 bump 2018-12-21 16:45:05 +09:00
968c820702 Add turkish translation 2018-12-21 14:15:05 +09:00
3061b4dfd2 Add comments 2018-12-21 13:33:26 +09:00
ed4de612dd Fix layout if customized to an absolute uri 2018-12-21 13:31:02 +09:00
d4bdd5fd9c Do not use absolute link to link theme files on layout.cshtml 2018-12-21 13:24:01 +09:00
8b71556425 Merge branch 'master' into 455-disablereg 2018-12-20 21:58:07 +01:00
ae6e1bfd85 Update UpdateCoinSwitchSettings.cshtml 2018-12-20 21:01:10 +01:00
6d1f3b73ef update link 2018-12-20 20:40:33 +01:00
0dcaf80c7f Changed disable register mechanism to apply policy setting after admin user created rather than using DB user count checks. 2018-12-20 20:39:48 +01:00
fc9cd5bdf0 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-20 17:57:04 +01:00
a434c45196 fix function names 2018-12-20 17:56:57 +01:00
87b316ec23 Merge pull request #439 from Kukks/grs-clightning
Add lightning icon for GRS
2018-12-20 22:43:28 +09:00
9c99ffae57 Lightning charge integration 2018-12-20 22:40:32 +09:00
30a3a84ec9 fix final issues with integration 2018-12-20 14:33:31 +01:00
d0f585df9d fix tests 2018-12-20 21:34:09 +09:00
bac2db5cda Add timeout to lightning tests 2018-12-20 21:27:08 +09:00
c35bf2f483 fix docker compose 2018-12-20 21:23:24 +09:00
2e04c5e39c Update docker-compose test 2018-12-20 21:20:30 +09:00
9dcf16e819 Add xunit diagnostic message 2018-12-20 21:00:06 +09:00
361d494cde Accept cookiefilepath as alternative to cookiefile for spark connection string 2018-12-20 20:24:06 +09:00
4d7015294e Fix configuration of lnd rest 2018-12-20 20:12:36 +09:00
5f16fb4668 report slow tests 2018-12-20 18:44:39 +09:00
4bf2228675 Show test logs in CircleCI 2018-12-20 18:35:32 +09:00
2ba823f192 Merge pull request #465 from Kukks/master
do not add ln payment if amount couldn't be parsed
2018-12-20 18:09:57 +09:00
27fa2d5b69 do not add ln payment if amount couldn't be parsed 2018-12-20 09:21:20 +01:00
47ef7661d8 Fix search for macaroon 2018-12-20 17:08:32 +09:00
3f6ff25322 update es-ES 2018-12-20 17:00:30 +09:00
f56c23009a bump 2018-12-20 16:57:59 +09:00
e80593fb7b Expose LND's other macaroon if possible 2018-12-20 16:52:04 +09:00
57324345ac Remove remaining of RestrictedMacaroon 2018-12-20 16:24:36 +09:00
73e280157d Show the gRPC cypher suites for gRPC consumption 2018-12-20 14:16:23 +09:00
cfaa5766ed Always allow user registration if there are no user records. 2018-12-19 20:03:27 +01:00
8b08db308b Merge branch 'master' into 455-disablereg 2018-12-19 19:40:13 +01:00
3e2ff55954 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-19 10:41:36 +01:00
70d1d0d230 Remove volumes before running tests 2018-12-19 15:50:20 +09:00
94e0048a3b Redirect users to docs.btcpayserver.org on home page 2018-12-19 15:30:10 +09:00
a34d1641b3 Set disable registration as default true. 2018-12-18 20:16:48 +01:00
40c645e433 coinswitch integration 2018-12-18 20:14:59 +01:00
b2e5415a35 coinswitch integration 2018-12-18 20:00:30 +01:00
365ee4cf0b Fixing CSV test now that we have new field / reorders 2018-12-18 12:35:59 -06:00
2b4603a234 coinswitch integration 2018-12-18 19:01:58 +01:00
ec23eae21d Ensuding that payments are always ordered by time for consistency 2018-12-18 11:56:51 -06:00
7a9229628a InvoiceDue field in export 2018-12-18 11:56:12 -06:00
9db5c0f375 Hack tests to make currency formatting work on linux 2018-12-19 00:28:06 +09:00
27bde55f54 work on building the viewmodel for crowdfund 2018-12-18 16:27:03 +01:00
2bb24282d2 Clean previous run with dock-compose 2018-12-19 00:15:33 +09:00
998472e463 Fix symbol display on linux 2018-12-19 00:11:15 +09:00
63ff46a768 cache docker on circleCI for tests 2018-12-18 23:27:57 +09:00
660f43e3b7 Add fast test for JPY formatting 2018-12-18 23:26:35 +09:00
0ba96aa4b8 Fix export tests 2018-12-18 23:24:22 +09:00
d85247d2ad Run tests inside container 2018-12-18 22:35:58 +09:00
9ca85ed365 Change column order 2018-12-18 21:44:51 +09:00
93113fd871 Fix payment exports to reflect correctly payment data, rename fields. 2018-12-18 21:35:52 +09:00
b5d360594a Merge remote-tracking branch 'origin/master' into feature/crowdfund 2018-12-18 13:29:22 +01:00
d5ae79c38c Add more information about status in the CSV export 2018-12-18 19:33:14 +09:00
7cf07b27e3 Invoice export should not prefix amounts with crypto code 2018-12-18 19:20:10 +09:00
bb0f986b0c Add additional test on euro formatting 2018-12-18 19:09:55 +09:00
2c2a85327f Add test logs 2018-12-18 01:02:27 +09:00
7bf03e497b In cart js, add space to symbol if needed (fix #450) 2018-12-18 00:38:59 +09:00
7a4dee3d38 Point of Sale returns correct currency information (#450) 2018-12-18 00:25:17 +09:00
7b27d6f0bb Merge branch 'mariodian-pos-product-management' 2018-12-15 23:40:04 +09:00
83dc95a0a7 Remove dollar sign in textbox 2018-12-15 23:39:45 +09:00
d60889f952 Merge branch 'pos-product-management' of https://github.com/mariodian/btcpayserver into mariodian-pos-product-management 2018-12-15 23:22:07 +09:00
8c9952973d Merge pull request #449 from sipsorcery/uxpwdreset
HTML formatting fix for issue #448.
2018-12-15 23:20:09 +09:00
00673bdb7f Fix product width on smaller screens 2018-12-14 16:16:08 +08:00
d039890a9b Create js-only product management in PoS 2018-12-14 16:03:02 +08:00
e3833914b3 Update AddDerivationScheme.cshtml 2018-12-13 16:18:01 +01:00
49cdec6961 Update PayButtonEnable.cshtml 2018-12-13 12:26:30 +01:00
3ad1834439 Better fix this as well or the user gets a blank page after the reset. 2018-12-12 22:39:37 +01:00
4b492eae85 HTML formatting fix for issue #448. 2018-12-12 22:29:40 +01:00
7fa1b65af0 initial commit 2018-12-11 16:36:25 +01:00
c00c95efcf initial coinswitch work 2018-12-11 12:47:38 +01:00
becf488714 Merge remote-tracking branch 'btcpayserver/master' into grs-clightning 2018-12-10 10:42:08 +01:00
580494fea7 add correct icon 2018-12-04 11:46:01 +01:00
338 changed files with 66203 additions and 11320 deletions

View File

@ -7,23 +7,16 @@ jobs:
- checkout
test:
machine: true
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
lsb_release -a
wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.1
dotnet --info
dotnet build /p:TreatWarningsAsErrors=true
cd BTCPayServer.Tests
dotnet test --filter Fast=Fast
docker-compose up -d dev
dotnet test --filter Integration=Integration
docker-compose down --v
docker-compose build
docker-compose run tests
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:

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

@ -34,6 +34,8 @@ using System.Security.Principal;
using System.Text;
using System.Threading;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
namespace BTCPayServer.Tests
{
@ -119,15 +121,17 @@ 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 });
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.ConfigureServices(s =>
{
s.AddLogging(l =>
{
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
@ -207,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

@ -0,0 +1,89 @@
using BTCPayServer.Controllers;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class CoinSwitchTests
{
public CoinSwitchTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanSetCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.StoreData.GetStoreBlob();
Assert.Null(storeBlob.CoinSwitchSettings);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.StoreData.GetStoreBlob();
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
updateModel.MerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanToggleCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
}
}
}
}

View File

@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.UnitTest1;
namespace BTCPayServer.Tests
{
public class CrowdfundTests
{
public CrowdfundTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateAndDeleteCrowdfundApp()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var user2 = tester.NewAccount();
user2.GrantAccess();
var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
Assert.Empty(appList.Apps);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanContributeOnlyWhenAllowed()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
//Scenario 1: Not Enabled - Not Allowed
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = false;
crowdfundViewModel.EndDate = null;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>();
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}, default));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
//Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
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));
//Scenario 3: Enabled But Start Date > Now - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}, default));
//Scenario 4: Enabled But End Date < Now - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate= DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
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
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate= DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
crowdfundViewModel.TargetAmount = 1;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
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));
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCrowdfundModel()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
Logs.Tester.LogInformation("We create an invoice with a hardcap");
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.Enabled = true;
crowdfundViewModel.EndDate = null;
crowdfundViewModel.TargetAmount = 100;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
Assert.Equal(0m, model.Info.CurrentAmount );
Assert.Equal(0m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(0m ,model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
Assert.Equal(1m, model.Info.PendingProgressPercentage);
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(1m, model.Info.CurrentAmount);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
});
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m));
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}
}
}
}

View File

@ -8,30 +8,27 @@ using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading.Channels;
using System.IO;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
namespace BTCPayServer.Tests
{
public class CustomServer : IDisposable
{
TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource();
Channel<JObject> _Requests = Channel.CreateUnbounded<JObject>();
public CustomServer()
{
{
var port = Utils.FreeTcpPort();
_Host = new WebHostBuilder()
.Configure(app =>
{
app.Run(req =>
{
while (_Act == null)
{
Thread.Sleep(10);
_Closed.Token.ThrowIfCancellationRequested();
}
_Act(req);
_Act = null;
_Evt.TrySetResult(true);
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
req.Response.StatusCode = 200;
return Task.CompletedTask;
});
@ -47,22 +44,24 @@ namespace BTCPayServer.Tests
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
}
Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act)
public async Task<JObject> GetNextRequest()
{
var source = new TaskCompletionSource<bool>();
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>();
_Evt = source;
_Act = act;
try
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
{
_Evt.Task.GetAwaiter().GetResult();
}
catch (TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
try
{
JObject req = null;
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
!_Requests.Reader.TryRead(out req))
{
}
return req;
}
catch (TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
}
}
}

View File

@ -1,12 +1,14 @@
FROM microsoft/dotnet:2.1.403-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
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
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
WORKDIR /app/BTCPayServer.Tests
RUN dotnet restore
# copies the rest of your code
COPY . ../.
ENTRYPOINT ["dotnet", "test"]
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj
COPY . .
RUN dotnet build
WORKDIR /source/BTCPayServer.Tests
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@ -0,0 +1,67 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
public class MockDelay : IDelay
{
class WaitObj
{
public DateTimeOffset Expiration;
public TaskCompletionSource<bool> CTS;
}
List<WaitObj> waits = new List<WaitObj>();
DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public async Task Wait(TimeSpan delay, CancellationToken cancellation)
{
WaitObj w = new WaitObj();
w.Expiration = _Now + delay;
w.CTS = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
w.CTS.TrySetCanceled();
}))
{
lock (waits)
{
waits.Add(w);
}
await w.CTS.Task;
}
}
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)
{
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 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

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class PaymentRequestTests
{
public PaymentRequestTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateViewUpdateAndDeletePaymentRequest()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount();
user2.GrantAccess();
var paymentRequestController = user.GetController<PaymentRequestController>();
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var id = (Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
//permission guard for guests editing
Assert
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
request.Title = "update";
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
Assert.False(string.IsNullOrEmpty(id));
Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
//Delete
Assert.IsType<ConfirmModel>(Assert
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
Assert
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanPayPaymentRequestWhenPossible()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
.ToString();
var actionResult = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
Assert.Equal("Checkout", actionResult.ActionName);
Assert.Equal("Invoice", actionResult.ControllerName);
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(1, invoice.Price);
request = new UpdatePaymentRequestViewModel()
{
Title = "original juice with expiry",
Currency = "BTC",
Amount = 1,
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
StoreId = user.StoreId,
Description = "description"
};
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
Assert
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
}
}
}
}

View File

@ -22,6 +22,8 @@ using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{
@ -84,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()
@ -152,6 +156,7 @@ namespace BTCPayServer.Tests
{
get; set;
}
public List<string> Stores { get; internal set; } = new List<string>();
public void Dispose()

View File

@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
namespace BTCPayServer.Tests
{
@ -58,6 +59,21 @@ namespace BTCPayServer.Tests
CreateStoreAsync().GetAwaiter().GetResult();
}
public void SetNetworkFeeMode(NetworkFeeMode mode)
{
ModifyStore((store) =>
{
store.NetworkFeeMode = mode;
});
}
public void ModifyStore(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
@ -83,10 +99,6 @@ namespace BTCPayServer.Tests
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationScheme = DerivationScheme.ToString(),

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
services:
tests:
@ -19,10 +19,10 @@ services:
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
expose:
- "80"
@ -36,7 +36,7 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.17.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -53,7 +53,7 @@ services:
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.17.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.2
image: nicolasdorier/nbxplorer:2.0.0.26
restart: unless-stopped
ports:
- "32838:32838"
@ -93,12 +93,13 @@ services:
- bitcoind
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.17.0
restart: unless-stopped
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
@ -106,9 +107,9 @@ services:
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
ports:
- "43782:43782"
- "28332:28332"
expose:
- "43782" # RPC
- "39388" # P2P
@ -118,7 +119,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6.2-3-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -144,7 +145,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.3
image: shesek/lightning-charge:0.4.6-standalone
restart: unless-stopped
environment:
NETWORK: regtest
@ -164,7 +165,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6.2-3-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -188,13 +189,13 @@ services:
- bitcoind
litecoind:
image: nicolasdorier/docker-litecoin:0.15.1
restart: unless-stopped
image: nicolasdorier/docker-litecoin:0.16.3
environment:
BITCOIN_EXTRA_ARGS: |
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
@ -221,13 +222,16 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:0.5-beta-2
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
@ -248,13 +252,16 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.5-beta-2
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpubrawblock=tcp://bitcoind:28332

View File

@ -0,0 +1,5 @@
#!/bin/sh
set -e
dotnet test --filter Fast=Fast --no-build
dotnet test --filter Integration=Integration --no-build -v n

View File

@ -1,3 +1,5 @@
{
"parallelizeTestCollections": false
}
"parallelizeTestCollections": false,
"longRunningTestSeconds": 60,
"diagnosticMessages": true
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinplus()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoinplus",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoinplus",
DefaultRateRules = new[]
{
"XBC_X = XBC_BTC * BTC_X",
"XBC_BTC = cryptopia(XBC_BTC)"
},
CryptoImagePath = "imlegacy/bitcoinplus.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
});
}
}
}

View File

@ -24,7 +24,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = cryptopia(BTX_BTC)"
"BTX_BTC = hitbtc(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",

View File

@ -26,7 +26,7 @@ namespace BTCPayServer
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
});

View File

@ -52,10 +52,13 @@ namespace BTCPayServer
InitBitcoinGold();
InitMonacoin();
InitDash();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.31</Version>
<Version>1.0.3.92</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -30,26 +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.3" />
<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="Hangfire" Version="1.6.20" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<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.73" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<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.1" />
<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" />
@ -68,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>
@ -126,6 +127,7 @@
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
</ItemGroup>
<ItemGroup>
@ -136,7 +138,10 @@
<Content Update="Views\Home\BitpayTranslator.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">

View File

@ -15,7 +15,6 @@ using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using Serilog.Events;
namespace BTCPayServer.Configuration
@ -37,8 +36,8 @@ namespace BTCPayServer.Configuration
{
get;
private set;
}
}
public string LogFile
{
get;
@ -49,11 +48,7 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public EndPoint SocksEndpoint { get; set; }
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
@ -61,6 +56,12 @@ namespace BTCPayServer.Configuration
set;
} = new List<NBXplorerConnectionSetting>();
public bool DisableRegistration
{
get;
private set;
}
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
@ -68,7 +69,7 @@ namespace BTCPayServer.Configuration
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
{
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
}
public void LoadArgs(IConfiguration conf)
@ -103,71 +104,57 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
if (connectionString.IsLegacy)
else
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
void externalLnd<T>(string code, string lndType)
{
var lightning = conf.GetOrDefault<string>(code, string.Empty);
if (lightning.Length != 0)
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
}
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if(spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
ExternalServices.Load(net.CryptoCode, conf);
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
var services = conf.GetOrDefault<string>("externalservices", null);
if(services != null)
if (services != null)
{
foreach(var service in services.Split(new[] { ';', ',' })
.Select(p => p.Split(':'))
.Where(p => p.Length == 2)
.Select(p => (Name: p[0], Link: p[1])))
foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase)))
.Where(p => p.SeparatorIndex != -1)
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
OtherExternalServices.AddOrReplace(service.Name, uri);
}
}
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
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))
@ -213,7 +200,7 @@ namespace BTCPayServer.Configuration
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))
@ -221,11 +208,12 @@ namespace BTCPayServer.Configuration
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
@ -252,12 +240,6 @@ namespace BTCPayServer.Configuration
settings.Username = "root";
}
}
else if (externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
@ -271,9 +253,9 @@ namespace BTCPayServer.Configuration
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
@ -286,11 +268,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
public bool BundleJsCss
{
get;
@ -302,14 +279,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
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,8 +40,11 @@ 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);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -50,7 +52,8 @@ namespace BTCPayServer.Configuration
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalspark", $"The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue);
}
return app;
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public abstract class ExternalLnd : ExternalService
{
public ExternalLnd(LightningConnectionString connectionString, string type)
{
ConnectionString = connectionString;
Type = type;
}
public string Type { get; set; }
public LightningConnectionString ConnectionString { get; set; }
}
public class ExternalLndGrpc : ExternalLnd
{
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
}
public class ExternalLndRest : ExternalLnd
{
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalServices : MultiValueDictionary<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalSpark : ExternalService
{
public SparkConnectionString ConnectionString { get; }
public ExternalSpark(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
}
}

View File

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
namespace BTCPayServer.Configuration
{
public class ExternalConnectionString
{
public Uri Server { get; set; }
public byte[] Macaroon { get; set; }
public Macaroons Macaroons { get; set; }
public string MacaroonFilePath { get; set; }
public string CertificateThumbprint { get; set; }
public string MacaroonDirectoryPath { get; set; }
public string APIToken { get; set; }
public string CookieFilePath { get; set; }
public string AccessKey { get; set; }
/// <summary>
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
{
var connectionString = this.Clone();
// Transform relative URI into absolute URI
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
{
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
}
connectionString.Server = serviceUri;
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
{
// Read the MacaroonDirectory
if (connectionString.MacaroonDirectoryPath != null)
{
try
{
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
connectionString.MacaroonDirectoryPath = null;
}
catch (Exception ex)
{
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
}
}
// Read the MacaroonFilePath
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
}
}
}
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
{
// Read access key from cookie file
if (connectionString.CookieFilePath != null)
{
string cookieFileContent = null;
bool isFake = false;
try
{
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
isFake = connectionString.CookieFilePath == "fake";
connectionString.CookieFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
}
if (serviceType == ExternalServiceTypes.RTL)
{
connectionString.AccessKey = cookieFileContent;
}
else if (serviceType == ExternalServiceTypes.Spark)
{
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
: cookieFileContent).Split(':');
if (cookie.Length >= 3)
{
connectionString.AccessKey = cookie[2];
}
else
{
throw new FormatException("Invalid cookiefile format");
}
}
else if (serviceType == ExternalServiceTypes.Charge)
{
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
}
}
}
return connectionString;
}
private Uri ToRelative(Uri absoluteUrlBase, string path)
{
if (path.StartsWith('/'))
path = path.Substring(1);
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
}
public ExternalConnectionString Clone()
{
return new ExternalConnectionString()
{
MacaroonFilePath = MacaroonFilePath,
CertificateThumbprint = CertificateThumbprint,
Macaroon = Macaroon,
MacaroonDirectoryPath = MacaroonDirectoryPath,
Server = Server,
APIToken = APIToken,
CookieFilePath = CookieFilePath,
AccessKey = AccessKey,
Macaroons = Macaroons?.Clone()
};
}
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new ExternalConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookieFilePath != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookieFilePath = kv[1];
break;
case "macaroondirectorypath":
resultTemp.MacaroonDirectoryPath = kv[1];
break;
case "certthumbprint":
resultTemp.CertificateThumbprint = kv[1];
break;
case "macaroonfilepath":
resultTemp.MacaroonFilePath = kv[1];
break;
case "api-token":
resultTemp.APIToken = kv[1];
break;
case "access-key":
resultTemp.AccessKey = kv[1];
break;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using Microsoft.Extensions.Configuration;
namespace BTCPayServer.Configuration
{
public class ExternalServices : List<ExternalService>
{
public void Load(string cryptoCode, IConfiguration configuration)
{
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (gRPC server)");
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (REST server)");
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Spark server)");
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"LND (Ride the Lightning server)");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
}
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
{
var setting = $"{cryptoCode}.external.{serviceName}";
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
if (connStr.Length != 0)
{
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
{
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
}
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
}
}
public ExternalService GetService(string serviceName, string cryptoCode)
{
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
}
public class ExternalService
{
public string DisplayName { get; set; }
public ExternalServiceTypes Type { get; set; }
public ExternalConnectionString ConnectionString { get; set; }
public string CryptoCode { get; set; }
public string ServiceName { get; set; }
}
public enum ExternalServiceTypes
{
LNDRest,
LNDGRPC,
Spark,
RTL,
Charge
}
}

View File

@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class SparkConnectionString
{
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
return false;
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
return false;
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
break;
case "cookiefile":
if (resultTemp.CookeFile != null)
return false;
resultTemp.CookeFile = kv[1];
break;
default:
return false;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -2,6 +2,7 @@
using BTCPayServer.Filters;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitpayClient;
@ -13,7 +14,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
[BitpayAPIConstraint()]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -34,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

@ -28,10 +28,11 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
Configuration.BTCPayServerOptions _Options;
ILogger _logger;
public AccountController(
@ -39,15 +40,17 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository)
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
_logger = Logs.PayServer;
}
@ -271,12 +274,20 @@ namespace BTCPayServer.Controllers
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
if(_Options.DisableRegistration)
{
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
policies.LockSubscription = true;
await _SettingsRepository.UpdateSetting(policies);
}
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
if (!policies.RequiresConfirmedEmail)
{
if(logon)
@ -436,8 +447,9 @@ namespace BTCPayServer.Controllers
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

View File

@ -0,0 +1,168 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class AppUpdated
{
public string AppId { get; set; }
public object Settings { get; set; }
public string StoreId { get; set; }
public override string ToString()
{
return String.Empty;
}
}
[HttpGet]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
{
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel()
{
Title = settings.Title,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
PerksTemplate = settings.PerksTemplate,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
UseAllStoreInvoices = app.TagAllInvoices,
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
}
catch
{
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
{
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
{
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
}
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
{
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)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var newSettings = new CrowdfundSettings()
{
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate,
TargetCurrency = vm.TargetCurrency,
Description = _htmlSanitizer.Sanitize( vm.Description),
EndDate = vm.EndDate,
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
SoundsEnabled = vm.SoundsEnabled,
DisqusShortname = vm.DisqusShortname,
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
};
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
await UpdateAppSettings(app);
_EventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
}
}
}

View File

@ -1,5 +1,8 @@
using System.Text;
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
@ -52,12 +55,16 @@ namespace BTCPayServer.Controllers
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
@ -65,8 +72,13 @@ namespace BTCPayServer.Controllers
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
public string CustomCSSLink { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
}
[HttpGet]
@ -79,15 +91,21 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomCSSLink = settings.CustomCSSLink
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl
};
if (HttpContext?.Request != null)
{
@ -108,7 +126,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
@ -130,11 +148,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.Template, vm.Currency);
_AppService.Parse(vm.Template, vm.Currency);
}
catch
{
@ -152,11 +170,14 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);
@ -171,8 +192,25 @@ namespace BTCPayServer.Controllers
ctx.Apps.Add(app);
ctx.Entry<AppData>(app).State = EntityState.Modified;
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
await ctx.SaveChangesAsync();
}
}
private int[] ListSplit(string list, string separator = ",")
{
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
}
else
{
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
list = charsToDestroy.Replace(list, "");
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
}
}
}
}

View File

@ -7,6 +7,8 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -24,19 +26,28 @@ namespace BTCPayServer.Controllers
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
AppService AppService)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_AppService = AppService;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private AppService _AppService;
[TempData]
public string StatusMessage { get; set; }
@ -44,7 +55,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
var apps = await _AppService.GetAllApps(GetUserId());
return View(new ListAppsViewModel()
{
Apps = apps
@ -58,7 +69,7 @@ namespace BTCPayServer.Controllers
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await DeleteApp(appData))
if (await _AppService.DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
@ -67,10 +78,15 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await GetOwnedStores();
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
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
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
@ -82,10 +98,15 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await GetOwnedStores();
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
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
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
@ -117,9 +138,17 @@ namespace BTCPayServer.Controllers
}
StatusMessage = "App successfully created";
CreatedAppId = id;
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
switch (appType)
{
case AppType.PointOfSale:
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
case AppType.Crowdfund:
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
default:
return RedirectToAction(nameof(ListApps));
}
}
[HttpGet]
@ -139,53 +168,10 @@ namespace BTCPayServer.Controllers
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
}
private async Task<StoreData[]> GetOwnedStores()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
private async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -1,20 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
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;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Ganss.XSS;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
@ -22,54 +31,184 @@ namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
public AppsPublicController(AppService AppService,
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppsHelper = appsHelper;
_AppService = AppService;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private AppsHelper _AppsHelper;
private AppService _AppService;
private InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(currency.Code) ?? _AppsHelper.Currencies.GetNumberFormatInfo("USD");
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
CurrencyCode = currency.Code,
CurrencySymbol = currency.Symbol,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
{
CurrencySymbol = string.IsNullOrEmpty(currency.Symbol) ? currency.Code : currency.Symbol,
Divisibility = currency.Divisibility,
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern)
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
Items = _AppService.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomCSSLink = settings.CustomCSSLink
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink,
AppId = appId
});
}
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
if (!hasEnoughSettingsToLoad)
{
if(!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
if (settings.Enabled) return View(appInfo);
if(!isAdmin)
return NotFound();
return View(appInfo);
}
[HttpPost]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
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) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _AppService.GetStore(app);
var title = settings.Title;
var price = request.Amount;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppService.Parse(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
NotificationEmail = settings.NotificationEmail,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) }, cancellationToken: cancellationToken);
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
}
else
{
return Ok(invoice.Data.Id);
}
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
@ -78,9 +217,10 @@ namespace BTCPayServer.Controllers
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
string choiceKey,
string posData = null, CancellationToken cancellationToken = default)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
@ -94,10 +234,11 @@ namespace BTCPayServer.Controllers
}
string title = null;
var price = 0.0m;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
var choices = _AppService.Parse(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
@ -112,136 +253,29 @@ namespace BTCPayServer.Controllers
price = amount;
title = settings.Title;
}
var store = await _AppsHelper.GetStore(app);
var store = await _AppService.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
ItemCode = choiceKey ?? string.Empty,
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
NotificationURL = string.IsNullOrEmpty(notificationUrl)? settings.NotificationUrl: notificationUrl,
NotificationEmail = settings.NotificationEmail,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
}
public class AppsHelper
{
ApplicationDbContextFactory _ContextFactory;
CurrencyNameTable _Currencies;
public CurrencyNameTable Currencies => _Currencies;
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
private string GetUserId()
{
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = c.GetDetailString("description"),
Id = c.Key,
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
return _UserManager.GetUserId(User);
}
}
}

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;
@ -12,7 +13,6 @@ using NBitpayClient;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
@ -33,9 +33,11 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
{
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}
[HttpGet]
@ -66,15 +68,15 @@ namespace BTCPayServer.Controllers
{
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var query = new InvoiceQuery()
{
Count = limit,
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
OrderId = orderId == null ? null : new[] { orderId },
ItemCode = itemCode == null ? null : new[] { itemCode },
Status = status == null ? null : new[] { status },
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};

View File

@ -13,6 +13,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
@ -20,6 +21,7 @@ using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
@ -63,6 +65,7 @@ namespace BTCPayServer.Controllers
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
@ -79,9 +82,9 @@ namespace BTCPayServer.Controllers
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = $"{accounting.Due} {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = $"{accounting.CryptoPaid} {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = $"{accounting.OverpaidHelper} {paymentMethodId.CryptoCode}";
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
@ -103,7 +106,7 @@ namespace BTCPayServer.Controllers
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
@ -185,9 +188,9 @@ namespace BTCPayServer.Controllers
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
//
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
@ -210,31 +213,46 @@ namespace BTCPayServer.Controllers
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
[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);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
bool isDefaultPaymentId = false;
if (paymentMethodId == null)
{
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
isDefaultPaymentId = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
if (network == null && isDefaultPaymentId)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultCrypto)
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
@ -243,7 +261,6 @@ namespace BTCPayServer.Controllers
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
@ -258,6 +275,11 @@ namespace BTCPayServer.Controllers
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
: null;
var changellyAmountDue = changelly != null
@ -268,17 +290,18 @@ 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),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en",
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(),
@ -304,11 +327,15 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
NetworkFee = paymentMethodDetails.GetTxFee(),
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
ChangellyEnabled = changelly != null,
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage?? 0,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
@ -338,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)
@ -360,9 +386,12 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/{invoiceId}/status")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return Json(model);
@ -370,6 +399,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
@ -415,6 +448,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
[Route("invoice/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if (!ModelState.IsValid)
@ -422,7 +456,7 @@ namespace BTCPayServer.Controllers
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok();
return Ok("{}");
}
[HttpGet]
@ -438,8 +472,12 @@ namespace BTCPayServer.Controllers
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Count = count;
invoiceQuery.Skip = skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
@ -451,32 +489,32 @@ namespace BTCPayServer.Controllers
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}",
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete()
});
}
model.Total = await counting;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var invoiceQuery = new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
});
return list;
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
};
return invoiceQuery;
}
[HttpGet]
@ -484,9 +522,12 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null)
{
var model = new InvoiceExport();
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
invoiceQuery.Count = int.MaxValue;
invoiceQuery.Skip = 0;
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
var res = model.Process(invoices, format);
var cd = new ContentDisposition
@ -520,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);
@ -556,7 +597,7 @@ namespace BTCPayServer.Controllers
try
{
var result = await CreateInvoiceCore(new Invoice()
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Amount.Value,
Currency = model.Currency,
@ -568,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));
@ -638,13 +679,13 @@ namespace BTCPayServer.Controllers
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
StatusMessage = "Invoice marked invalid";
}
else if(newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete"));
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
StatusMessage = "Invoice marked complete";
}
return RedirectToAction(nameof(ListInvoices));
@ -664,9 +705,9 @@ namespace BTCPayServer.Controllers
public class PosDataParser
{
public static Dictionary<string, string> ParsePosData(string posData)
public static Dictionary<string, object> ParsePosData(string posData)
{
var result = new Dictionary<string,string>();
var result = new Dictionary<string,object>();
if (string.IsNullOrEmpty(posData))
{
return result;
@ -674,7 +715,6 @@ namespace BTCPayServer.Controllers
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
@ -682,7 +722,14 @@ namespace BTCPayServer.Controllers
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count(); i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.Add(item.Key, item.Value.ToString());

View File

@ -3,13 +3,16 @@ 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;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -60,7 +63,7 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
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();
@ -68,13 +71,12 @@ namespace BTCPayServer.Controllers
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity
{
Version = InvoiceEntity.Lastest_Version,
InvoiceTime = DateTimeOffset.UtcNow
};
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
@ -82,10 +84,17 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
if (invoice.NotificationURL != null &&
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
{
entity.NotificationURL = notificationUri.AbsoluteUri;
}
entity.NotificationEmail = invoice.NotificationEmail;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
if (additionalTags != null)
entity.InternalTags.AddRange(additionalTags);
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if (entity?.BuyerInformation?.BuyerEmail != null)
@ -94,7 +103,24 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
int divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
@ -105,6 +131,17 @@ namespace BTCPayServer.Controllers
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
{
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
.Where(c => c.Value.Enabled)
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
.ToHashSet();
excludeFilter = PaymentFilter.Or(excludeFilter,
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
}
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
@ -118,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)
@ -146,7 +183,7 @@ namespace BTCPayServer.Controllers
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());
@ -157,9 +194,29 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
foreach (var app in await getAppsTaggingStore)
{
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
}
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" };
}
@ -187,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)];
@ -199,10 +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);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
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;
@ -230,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;
}
}
@ -241,7 +302,7 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.TxFee = paymentMethod.NextNetworkFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class Macaroons
{
public class Macaroon
{
public Macaroon(byte[] bytes)
{
Bytes = bytes;
Hex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(bytes);
}
public string Hex { get; set; }
public byte[] Bytes { get; set; }
}
public static async Task<Macaroons> GetFromDirectoryAsync(string directoryPath)
{
if (directoryPath == null)
throw new ArgumentNullException(nameof(directoryPath));
Macaroons macaroons = new Macaroons();
if (!Directory.Exists(directoryPath))
throw new DirectoryNotFoundException("Macaroons directory not found");
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
{
try
{
switch (Path.GetFileName(file))
{
case "admin.macaroon":
macaroons.AdminMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
case "readonly.macaroon":
macaroons.ReadonlyMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
case "invoice.macaroon":
macaroons.InvoiceMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
default:
break;
}
}
catch { }
}
return macaroons;
}
public Macaroons Clone()
{
return new Macaroons()
{
AdminMacaroon = AdminMacaroon,
InvoiceMacaroon = InvoiceMacaroon,
ReadonlyMacaroon = ReadonlyMacaroon
};
}
public Macaroon ReadonlyMacaroon { get; set; }
public Macaroon InvoiceMacaroon { get; set; }
public Macaroon AdminMacaroon { get; set; }
}
}

View File

@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
@ -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;
@ -156,8 +157,7 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(Index));
}

View File

@ -0,0 +1,340 @@
using System;
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;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
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
{
[Route("payment-requests")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
public class PaymentRequestController : Controller
{
private readonly InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
private readonly StoreRepository _StoreRepository;
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly HtmlSanitizer _htmlSanitizer;
public PaymentRequestController(
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_StoreRepository = storeRepository;
_PaymentRequestRepository = paymentRequestRepository;
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_htmlSanitizer = htmlSanitizer;
}
[HttpGet]
[Route("")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
{
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
UserId = GetUserId(), Skip = skip, Count = count
});
return View(new ListPaymentRequestsViewModel()
{
Skip = skip,
StatusMessage = statusMessage,
Count = count,
Total = result.Total,
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
});
}
[HttpGet]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
{
SelectList stores = null;
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
if (!stores.Any())
{
return RedirectToAction("GetPaymentRequests",
new
{
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
}
});
}
return View(new UpdatePaymentRequestViewModel(data)
{
Stores = stores,
StatusMessage = statusMessage
});
}
[HttpPost]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
{
if (string.IsNullOrEmpty(viewModel.Currency) ||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
if (!ModelState.IsValid)
{
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
return View(viewModel);
}
if (data == null)
{
data = new PaymentRequestData();
}
data.StoreDataId = viewModel.StoreId;
var blob = data.GetBlob();
blob.Title = viewModel.Title;
blob.Email = viewModel.Email;
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
blob.Amount = viewModel.Amount;
blob.ExpiryDate = viewModel.ExpiryDate;
blob.Currency = viewModel.Currency;
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
blob.CustomCSSLink = viewModel.CustomCSSLink;
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob);
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
_EventAggregator.Publish(new PaymentRequestUpdated()
{
Data = data,
PaymentRequestId = data.Id
});
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
}
[HttpGet]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
{
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null)
{
return NotFound();
}
var blob = data.GetBlob();
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
Action = "Delete"
});
}
[HttpPost]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequest(string id)
{
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
if (result)
{
return RedirectToAction("GetPaymentRequests",
new {StatusMessage = "Payment request successfully removed"});
}
else
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage =
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
});
}
}
[HttpGet]
[Route("{id}")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequest(string id)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
return View(result);
}
[HttpGet]
[Route("{id}/pay")]
[AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
if (result.AmountDue <= 0)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has already been settled.");
}
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has expired");
}
var statusesAllowedToDisplay = new List<InvoiceStatus>()
{
InvoiceStatus.New
};
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
statusesAllowedToDisplay.Contains(status));
if (validInvoice != null)
{
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
}
return Ok(validInvoice.Id);
}
if (result.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(result.AmountDue, amount.Value);
else
amount = result.AmountDue;
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
var blob = pr.GetBlob();
var store = pr.StoreData;
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
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.Value,
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = redirectUrl,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
cancellationToken: cancellationToken))
.Data.Id;
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
}
return Ok(newInvoiceId);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
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,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
@ -27,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)
@ -45,7 +47,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Price,
Currency = model.Currency,
@ -55,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

@ -0,0 +1,89 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Controllers
{
[Route("embed/{storeId}/{cryptoCode}/ln")]
[AllowAnonymous]
public class PublicLightningNodeInfoController : Controller
{
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
private readonly StoreRepository _StoreRepository;
public PublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
{
_BtcPayNetworkProvider = btcPayNetworkProvider;
_LightningLikePaymentHandler = lightningLikePaymentHandler;
_StoreRepository = storeRepository;
}
[HttpGet]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
{
var store = await _StoreRepository.FindStore(storeId);
if (store == null)
return NotFound();
try
{
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
network);
return View(new ShowLightningNodeInfoViewModel()
{
Available = true,
NodeInfo = nodeInfo.ToString(),
CryptoCode = cryptoCode,
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network)
});
}
catch (Exception)
{
return View(new ShowLightningNodeInfoViewModel() {Available = false, CryptoCode = cryptoCode});
}
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return "/" + res;
}
}
public class ShowLightningNodeInfoViewModel
{
public string NodeInfo { get; set; }
public bool Available { get; set; }
public string CryptoCode { get; set; }
public string CryptoImage { get; set; }
}
}

View File

@ -12,11 +12,14 @@ using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
using System.Threading;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
[EnableCors(CorsPolicies.All)]
public class RateController : Controller
{
RateFetcher _RateProviderFactory;
@ -43,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();
@ -62,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;
@ -73,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;
@ -86,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;
@ -116,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)
@ -137,15 +139,10 @@ namespace BTCPayServer.Controllers
if (currencyPairs == null)
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
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;
}
@ -166,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

@ -26,7 +26,7 @@ using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers
{
@ -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")]
@ -170,7 +173,7 @@ namespace BTCPayServer.Controllers
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
@ -208,8 +211,8 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
@ -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();
@ -253,6 +263,13 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Maintenance));
}
private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
{
if (IPAddress.TryParse(domainOrIP, out var ip))
return Task.FromResult(new[] { ip });
return Dns.GetHostAddressesAsync(domainOrIP);
}
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
[HttpGet]
[Route("runid")]
@ -345,22 +362,27 @@ namespace BTCPayServer.Controllers
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var isAdmin = IsAdmin(roles);
bool updated = false;
if (isAdmin != viewModel.IsAdmin)
viewModel.StatusMessage = "";
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (!viewModel.IsAdmin && admins.Count == 1)
{
viewModel.StatusMessage = "This is the only Admin, so their role can't be removed until another Admin is added.";
return View(viewModel); // return
}
var roles = await _UserManager.GetRolesAsync(user);
if (viewModel.IsAdmin != IsAdmin(roles))
{
if (viewModel.IsAdmin)
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
else
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
updated = true;
}
if (updated)
{
viewModel.StatusMessage = "User successfully updated";
}
return View(viewModel);
}
@ -371,12 +393,28 @@ namespace BTCPayServer.Controllers
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
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"));
}
else
{
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
"This user will be permanently deleted",
"Delete"));
}
}
[Route("server/users/{userId}/delete")]
@ -399,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()
{
@ -425,138 +456,147 @@ namespace BTCPayServer.Controllers
public IActionResult Services()
{
var result = new ServicesViewModel();
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
result.ExternalServices = _Options.ExternalServices;
foreach (var externalService in _Options.OtherExternalServices)
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = grpcService.Type,
Action = nameof(LndServices),
Index = i++,
});
}
i = 0;
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkServices),
Index = i++,
});
}
}
foreach(var externalService in _Options.ExternalServices)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = externalService.Key,
Link = this.Request.GetRelativePath(externalService.Value)
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
});
}
if(_Options.SSHSettings != null)
if (_Options.SSHSettings != null)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = "SSH",
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);
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
[Route("server/services/{serviceName}/{cryptoCode}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if(spark == null)
{
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (service == null)
return NotFound();
}
SparkServicesViewModel vm = new SparkServicesViewModel();
vm.ShowQR = showQR;
try
{
var cookie = (spark.CookeFile == "fake"
? "fake:fake:fake" // If we are testing, it should not crash
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
if (cookie.Length >= 3)
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
switch (service.Type)
{
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
case ExternalServiceTypes.Charge:
return LightningChargeServices(service, connectionString, showQR);
case ExternalServiceTypes.RTL:
case ExternalServiceTypes.Spark:
if (connectionString.AccessKey == null)
{
StatusMessage = $"Error: The access key of the service is not set";
return RedirectToAction(nameof(Services));
}
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = service.DisplayName;
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
return View("LightningWalletServices", vm);
case ExternalServiceTypes.LNDGRPC:
case ExternalServiceTypes.LNDRest:
return LndServices(service, connectionString, nonce);
default:
throw new NotSupportedException(service.Type.ToString());
}
}
catch(Exception ex)
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
public IActionResult LndServices(string cryptoCode, int index, uint? nonce)
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
{
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = connectionString.Server.AbsoluteUri;
vm.APIToken = connectionString.APIToken;
var builder = new UriBuilder(connectionString.Server);
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
return View(nameof(LightningChargeServices), vm);
}
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LndGrpcServicesViewModel();
if (external.ConnectionType == LightningConnectionType.LndGRPC)
if (service.Type == ExternalServiceTypes.LNDGRPC)
{
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
model.SSL = connectionString.Server.Scheme == "https";
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if(external.ConnectionType == LightningConnectionType.LndREST)
else if (service.Type == ExternalServiceTypes.LNDRest)
{
model.Uri = external.BaseUri.AbsoluteUri;
model.Uri = connectionString.Server.AbsoluteUri;
model.ConnectionType = "REST";
}
if (external.CertificateThumbprint != null)
if (connectionString.CertificateThumbprint != null)
{
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
model.CertificateThumbprint = connectionString.CertificateThumbprint;
}
if (external.Macaroon != null)
if (connectionString.Macaroon != null)
{
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
}
if (external.RestrictedMacaroon != null)
{
model.RestrictedMacaroon = Encoders.Hex.EncodeData(external.RestrictedMacaroon);
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
}
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
if (nonce != null)
{
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
model.QRCode = $"config={model.QRCodeLink}";
}
}
return View(model);
return View(nameof(LndServices), model);
}
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
{
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
}
[Route("lnd-config/{configKey}/lnd.config")]
@ -569,78 +609,62 @@ namespace BTCPayServer.Controllers
return Json(conf);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
[Route("server/services/{serviceName}/{cryptoCode}")]
[HttpPost]
public IActionResult LndServicesPost(string cryptoCode, int index)
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
{
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
LightningConfigurations confs = new LightningConfigurations();
if (external.ConnectionType == LightningConnectionType.LndGRPC)
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
LightningConfiguration conf = new LightningConfiguration();
conf.Type = "grpc";
conf.ChainType = _Options.NetworkType.ToString();
conf.CryptoCode = cryptoCode;
conf.Host = external.BaseUri.DnsSafeHost;
conf.Port = external.BaseUri.Port;
conf.SSL = external.BaseUri.Scheme == "https";
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
conf.RestrictedMacaroon = external.RestrictedMacaroon == null ? null : Encoders.Hex.EncodeData(external.RestrictedMacaroon);
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
confs.Configurations.Add(conf);
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
else if (external.ConnectionType == LightningConnectionType.LndREST)
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (service == null)
return NotFound();
ExternalConnectionString connectionString = null;
try
{
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
LightningConfigurations confs = new LightningConfigurations();
if (service.Type == ExternalServiceTypes.LNDGRPC)
{
LightningConfiguration grpcConf = new LightningConfiguration();
grpcConf.Type = "grpc";
grpcConf.Host = connectionString.Server.DnsSafeHost;
grpcConf.Port = connectionString.Server.Port;
grpcConf.SSL = connectionString.Server.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (service.Type == ExternalServiceTypes.LNDRest)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.ChainType = _Options.NetworkType.ToString();
restconf.CryptoCode = cryptoCode;
restconf.Uri = external.BaseUri.AbsoluteUri;
restconf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
restconf.RestrictedMacaroon = external.RestrictedMacaroon == null ? null : Encoders.Hex.EncodeData(external.RestrictedMacaroon);
restconf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
restconf.Uri = connectionString.Server.AbsoluteUri;
confs.Configurations.Add(restconf);
}
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
else
throw new NotSupportedException(service.Type.ToString());
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
commonConf.ChainType = _Options.NetworkType.ToString();
commonConf.CryptoCode = cryptoCode;
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if (connectionString == null)
return null;
connectionString = connectionString.Clone();
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch
{
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
return null;
}
}
if (connectionString.RestrictedMacaroonFilePath != null)
{
try
{
connectionString.RestrictedMacaroon = System.IO.File.ReadAllBytes(connectionString.RestrictedMacaroonFilePath);
}
catch
{
Logs.Configuration.LogWarning($"{cryptoCode}: The restrictedmacaroon file path of the external LND grpc config was not found ({connectionString.RestrictedMacaroonFilePath})");
}
connectionString.RestrictedMacaroonFilePath = null;
}
return connectionString;
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
}
[Route("server/services/ssh")]
@ -655,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);
@ -679,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";
@ -745,7 +780,8 @@ namespace BTCPayServer.Controllers
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file)) return View("Logs", vm);
if (string.IsNullOrEmpty(file))
return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
try

View File

@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers
string storeId,
string cryptoCode,
string command,
int account = 0)
string keyPath = "")
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
@ -67,7 +67,10 @@ namespace BTCPayServer.Controllers
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
var k = KeyPath.Parse(keyPath);
if (k.Indexes.Length == 0)
throw new FormatException("Invalid key path");
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
result = getxpubResult;
}
}
@ -171,7 +174,8 @@ namespace BTCPayServer.Controllers
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
store.SetStoreBlob(storeBlob);
}
catch

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

@ -0,0 +1,74 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/coinswitch")]
public IActionResult UpdateCoinSwitchSettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
UpdateCoinSwitchSettingsViewModel vm = new UpdateCoinSwitchSettingsViewModel();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
{
var existing = store.GetStoreBlob().CoinSwitchSettings;
if (existing == null) return;
vm.MerchantId = existing.MerchantId;
vm.Enabled = existing.Enabled;
vm.Mode = existing.Mode;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
}
[HttpPost]
[Route("{storeId}/coinswitch")]
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var coinSwitchSettings = new CoinSwitchSettings()
{
MerchantId = vm.MerchantId,
Enabled = vm.Enabled,
Mode = vm.Mode,
AmountMarkupPercentage = vm.AmountMarkupPercentage
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.CoinSwitchSettings = coinSwitchSettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "CoinSwitch settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
default:
return View(vm);
}
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[Route("{storeId}/emails")]
public IActionResult Emails()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("{storeId}/emails")]
[HttpPost]
public async Task<IActionResult> Emails(string storeId, EmailsViewModel model, string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
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";
}
catch (Exception ex)
{
model.StatusMessage = "Error: " + ex.Message;
}
return View(model);
}
else // if(command == "Save")
{
var storeBlob = store.GetStoreBlob();
storeBlob.EmailSettings = model.Settings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Email settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
}
}
}
}

View File

@ -27,7 +27,8 @@ namespace BTCPayServer.Controllers
LightningNodeViewModel vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
StoreId = storeId
};
SetExistingValues(store, vm);
return View(vm);
@ -154,7 +155,7 @@ namespace BTCPayServer.Controllers
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.Test(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;
@ -10,7 +11,9 @@ using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -94,6 +97,11 @@ namespace BTCPayServer.Controllers
{
get; set;
}
[TempData]
public bool StoreNotConfigured
{
get; set;
}
[HttpGet]
[Route("{storeId}/users")]
@ -165,7 +173,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove access to {user.Email}?",
Description = $"Are you sure you want to remove store access for {user.Email}?",
Action = "Delete"
});
}
@ -181,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);
@ -212,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))
@ -260,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)
{
@ -324,16 +347,34 @@ namespace BTCPayServer.Controllers
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
SetCryptoCurrencies(vm, StoreData);
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)
{
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
.Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray();
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
vm.DefaultPaymentMethod = chosen?.Value;
}
private string GetDisplayName(PaymentMethodId paymentMethodId)
{
var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode;
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
display : $"{display} (Lightning)";
}
[HttpPost]
[Route("{storeId}/checkout")]
@ -358,25 +399,27 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
var blob = StoreData.GetStoreBlob();
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
{
needUpdate = true;
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
StoreData.SetDefaultPaymentId(defaultPaymentMethodId);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
SetCryptoCurrencies(model, StoreData);
model.SetLanguages(_LangService, model.DefaultLang);
if (!ModelState.IsValid)
{
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;
@ -406,7 +449,7 @@ namespace BTCPayServer.Controllers
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
@ -464,6 +507,14 @@ namespace BTCPayServer.Controllers
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
Enabled = coinSwitchEnabled,
Action = nameof(UpdateCoinSwitchSettings),
Provider = "CoinSwitch"
});
}
[HttpPost]
@ -489,7 +540,7 @@ namespace BTCPayServer.Controllers
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
@ -557,6 +608,7 @@ namespace BTCPayServer.Controllers
var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
model.StatusMessage = StatusMessage;
model.StoreNotConfigured = StoreNotConfigured;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Facade,
@ -784,6 +836,10 @@ namespace BTCPayServer.Controllers
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
.Count() == 0;
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;

View File

@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
{
if (walletId?.StoreId == null)
return NotFound();
@ -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;
@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex) { model.RateError = ex.Message; }
}
model.AdvancedMode = advancedMode;
return View(model);
}
@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm)
WalletId walletId, WalletSendModel vm, string command = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
if (command == "noob" || command == "expert")
{
ModelState.Clear();
vm.AdvancedMode = command == "expert";
return View(vm);
}
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange
});
}
@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers
// getxpub
int account = 0,
// sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
@ -410,8 +421,8 @@ namespace BTCPayServer.Controllers
return NotFound();
var cryptoCode = walletId.CryptoCode;
var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase;
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
@ -436,7 +447,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -476,15 +487,6 @@ namespace BTCPayServer.Controllers
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(derivationScheme);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
result = new GetInfoResult();
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
@ -496,9 +498,32 @@ namespace BTCPayServer.Controllers
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var keypaths = new Dictionary<Script, KeyPath>();
List<Coin> availableCoins = new List<Coin>();
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins.Add(c.Coin);
}
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
var changeAddress = await change;
var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
// Some deployment have the wallet root key path saved in the store blob
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
{
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
throw new HardwareWalletException($"This store is not configured to use this ledger");
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData);
}
retry:
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
@ -514,15 +539,9 @@ namespace BTCPayServer.Controllers
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
builder.AddCoins(availableCoins);
foreach (var element in send)
{
@ -530,6 +549,7 @@ namespace BTCPayServer.Controllers
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
@ -546,13 +566,15 @@ namespace BTCPayServer.Controllers
}
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
if (noChange && hasChange)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
subsctractFeesValue = true;
goto retry;
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();

View File

@ -23,6 +23,7 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool TagAllInvoices { get; set; }
public string Settings { get; set; }
public T GetSettings<T>() where T : class, new()

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -54,6 +55,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public DbSet<PaymentRequestData> PaymentRequests
{
get; set;
}
public DbSet<StoreData> Stores
{
@ -204,6 +210,15 @@ namespace BTCPayServer.Data
o.UniqueId
#pragma warning restore CS0618
});
builder.Entity<PaymentRequestData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PaymentRequests)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);
}
}
}

View File

@ -3,8 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
@ -98,15 +96,5 @@ namespace BTCPayServer.Data
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
builder.UseMemoryStorage();
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
//if (_Type == DatabaseType.Sqlite)
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
//else if (_Type == DatabaseType.Postgres)
// builder.UsePostgreSqlStorage(_ConnectionString);
}
}
}

View File

@ -18,8 +18,11 @@ using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.Data
{
@ -39,6 +42,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public List<PaymentRequestData> PaymentRequests
{
get; set;
}
public List<InvoiceData> Invoices { get; set; }
@ -54,7 +62,6 @@ namespace BTCPayServer.Data
get;
set;
}
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
@ -193,7 +200,7 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetDefaultCrypto instead")]
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
@ -202,13 +209,32 @@ namespace BTCPayServer.Data
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
public PaymentMethodId GetDefaultPaymentId(BTCPayNetworkProvider networks)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
PaymentMethodId[] paymentMethodIds = GetEnabledPaymentIds(networks);
var defaultPaymentId = string.IsNullOrEmpty(DefaultCrypto) ? null : PaymentMethodId.Parse(DefaultCrypto);
var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ??
paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ??
paymentMethodIds.FirstOrDefault();
return chosen;
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
public PaymentMethodId[] GetEnabledPaymentIds(BTCPayNetworkProvider networks)
{
DefaultCrypto = defaultCryptoCurrency;
var excludeFilter = GetStoreBlob().GetExcludedPaymentMethods();
var paymentMethodIds = GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
.Where(a => !excludeFilter.Match(a))
.OrderByDescending(a => a.CryptoCode == "BTC")
.ThenBy(a => a.CryptoCode)
.ThenBy(a => a.PaymentType == PaymentTypes.LightningLike ? 1 : 0)
.ToArray();
return paymentMethodIds;
}
public void SetDefaultPaymentId(PaymentMethodId defaultPaymentId)
{
DefaultCrypto = defaultPaymentId.ToString();
}
#pragma warning restore CS0618
@ -249,6 +275,12 @@ namespace BTCPayServer.Data
}
}
public enum NetworkFeeMode
{
MultiplePaymentsOnly,
Always,
Never
}
public class StoreBlob
{
public StoreBlob()
@ -258,12 +290,42 @@ namespace BTCPayServer.Data
PaymentTolerance = 0;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
[Obsolete("Use NetworkFeeMode instead")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? NetworkFeeDisabled
{
get; set;
}
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public NetworkFeeMode NetworkFeeMode
{
get;
set;
}
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)]
@ -287,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; }
@ -305,6 +368,7 @@ namespace BTCPayServer.Data
public bool AnyoneCanInvoice { get; set; }
public ChangellySettings ChangellySettings { get; set; }
public CoinSwitchSettings CoinSwitchSettings { get; set; }
string _LightningDescriptionTemplate;
@ -366,6 +430,25 @@ namespace BTCPayServer.Data
[Obsolete("Use GetExcludedPaymentMethods instead")]
public string[] ExcludedPaymentMethods { get; set; }
#pragma warning disable CS0618 // Type or member is obsolete
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
{
if (keyPath == null)
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
else
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
}
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
{
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
return KeyPath.Parse(k);
return null;
}
#pragma warning restore CS0618 // Type or member is obsolete
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{

View File

@ -8,17 +8,31 @@ namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
public const string Created = "invoice_created";
public const string ReceivedPayment = "invoice_receivedPayment";
public const string MarkedCompleted = "invoice_markedComplete";
public const string MarkedInvalid= "invoice_markedInvalid";
public const string Expired= "invoice_expired";
public const string ExpiredPaidPartial= "invoice_expiredPaidPartial";
public const string PaidInFull= "invoice_paidInFull";
public const string PaidAfterExpiration= "invoice_paidAfterExpiration";
public const string FailedToConfirm= "invoice_failedToConfirm";
public const string Confirmed= "invoice_confirmed";
public const string Completed= "invoice_completed";
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
{
Invoice = invoice;
EventCode = code;
Name = name;
}
public Models.InvoiceResponse Invoice { get; set; }
public InvoiceEntity Invoice { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }
public PaymentEntity Payment { get; set; }
public override string ToString()
{
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";

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
{
@ -61,6 +62,23 @@ namespace BTCPayServer
}
return value;
}
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
{
if (value != 0m)
{
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
}
return value;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
@ -108,6 +126,12 @@ namespace BTCPayServer
return str;
return str + "/";
}
public static string WithStartingSlash(this string str)
{
if (str.StartsWith("/", StringComparison.InvariantCulture))
return str;
return $"/{str}";
}
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
{
@ -142,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(
@ -168,6 +217,13 @@ namespace BTCPayServer
request.Path.ToUriComponent());
}
/// <summary>
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
/// If 'toto' and RootPath is empty returns '/toto'
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string GetRelativePath(this HttpRequest request, string path)
{
if (path.Length > 0 && path[0] != '/')
@ -177,6 +233,27 @@ namespace BTCPayServer
path);
}
/// <summary>
/// If 'https://example.com/toto' returns 'https://example.com/toto'
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
/// If 'toto' and RootPath is empty returns '/toto'
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
{
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
uri.IsAbsoluteUri)
return path;
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
request.PathBase.ToUriComponent(),
path);
}
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
{
bool isRelative =
@ -185,6 +262,31 @@ namespace BTCPayServer
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
}
/// <summary>
/// Will return an absolute URL.
/// If `relativeOrAsbolute` is absolute, returns it.
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
/// </summary>
/// <param name="request"></param>
/// <param name="relativeOrAbsolte"></param>
/// <returns></returns>
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
{
if (relativeOrAbsolute == null)
{
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()), UriKind.Absolute);
}
if (relativeOrAbsolute.IsAbsoluteUri)
return relativeOrAbsolute;
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
}
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{
services.Configure<BTCPayServerOptions>(o =>
@ -227,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);
@ -272,5 +350,15 @@ namespace BTCPayServer
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
return res;
}
public static string TrimEnd(this string input, string suffixToRemove,
StringComparison comparisonType) {
if (input != null && suffixToRemove != null
&& input.EndsWith(suffixToRemove, comparisonType)) {
return input.Substring(0, input.Length - suffixToRemove.Length);
}
else return input;
}
}
}

View File

@ -10,9 +10,9 @@ namespace BTCPayServer.Services
{
public static class EmailSenderExtensions
{
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link)
{
return emailSender.SendEmailAsync(email, "Confirm your email",
emailSender.SendEmail(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}

View File

@ -12,13 +12,32 @@ namespace BTCPayServer.Filters
{
Value = value;
}
public string Value
public XFrameOptionsAttribute(XFrameOptions type, string allowFrom = null)
{
get; set;
switch (type)
{
case XFrameOptions.Deny:
Value = "deny";
break;
case XFrameOptions.SameOrigin:
Value = "deny";
break;
case XFrameOptions.AllowFrom:
Value = $"allow-from {allowFrom}";
break;
case XFrameOptions.AllowAll:
Value = "allow-all";
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public string Value { get; set; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
@ -28,5 +47,13 @@ namespace BTCPayServer.Filters
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
public enum XFrameOptions
{
Deny,
SameOrigin,
AllowFrom,
AllowAll
}
}
}

View File

@ -0,0 +1,142 @@
using System;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
using NicolasDorier.RateLimits;
namespace BTCPayServer.HostedServices
{
public class BackgroundJobSchedulerHostedService : IHostedService
{
public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient)
{
BackgroundJobClient = (BackgroundJobClient)backgroundJobClient;
}
public BackgroundJobClient BackgroundJobClient { get; }
Task _Loop;
public Task StartAsync(CancellationToken cancellationToken)
{
_Stop = new CancellationTokenSource();
_Loop = BackgroundJobClient.ProcessJobs(_Stop.Token);
return Task.CompletedTask;
}
CancellationTokenSource _Stop;
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_Stop == null)
return;
_Stop.Cancel();
try
{
await _Loop;
}
catch (OperationCanceledException)
{
}
try
{
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
catch (OperationCanceledException)
{
}
}
}
public class BackgroundJobClient : IBackgroundJobClient
{
class BackgroundJob
{
public Func<CancellationToken, Task> Action;
public TimeSpan Delay;
public IDelay DelayImplementation;
public BackgroundJob(Func<CancellationToken, Task> action, TimeSpan delay, IDelay delayImplementation)
{
this.Action = action;
this.Delay = delay;
this.DelayImplementation = delayImplementation;
}
public async Task Run(CancellationToken cancellationToken)
{
await DelayImplementation.Wait(Delay, cancellationToken);
await Action(cancellationToken);
}
}
public IDelay Delay { get; set; } = TaskDelay.Instance;
public int GetExecutingCount()
{
lock (_Processing)
{
return _Processing.Count();
}
}
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
HashSet<Task> _Processing = new HashSet<Task>();
public void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn)
{
_Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay));
}
public async Task WaitAllRunning(CancellationToken cancellationToken)
{
Task[] processing = null;
lock (_Processing)
{
if (_Processing.Count == 0)
return;
processing = _Processing.ToArray();
}
try
{
await Task.WhenAll(processing).WithCancellation(cancellationToken);
}
catch (Exception) when (!cancellationToken.IsCancellationRequested)
{
}
}
public async Task ProcessJobs(CancellationToken cancellationToken)
{
while (await _Jobs.Reader.WaitToReadAsync(cancellationToken))
{
if (_Jobs.Reader.TryRead(out var job))
{
var processing = job.Run(cancellationToken);
lock (_Processing)
{
_Processing.Add(processing);
}
_ = processing.ContinueWith(t =>
{
if (t.IsFaulted)
{
Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running");
}
lock (_Processing)
{
_Processing.Remove(processing);
}
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
}
}
}
}

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

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.HostedServices
{
public class EventHostedServiceBase : IHostedService
{
private readonly EventAggregator _EventAggregator;
private List<IEventAggregatorSubscription> _Subscriptions;
private CancellationTokenSource _Cts;
public EventHostedServiceBase(EventAggregator eventAggregator)
{
_EventAggregator = eventAggregator;
}
Channel<object> _Events = Channel.CreateUnbounded<object>();
public async Task ProcessEvents(CancellationToken cancellationToken)
{
while (await _Events.Reader.WaitToReadAsync(cancellationToken))
{
if (_Events.Reader.TryRead(out var evt))
{
try
{
await ProcessEvent(evt, cancellationToken);
}
catch when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Unhandled exception in {this.GetType().Name}");
}
}
}
}
protected virtual Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected virtual void SubscibeToEvents()
{
}
protected void Subscribe<T>()
{
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
}
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_Subscriptions = new List<IEventAggregatorSubscription>();
SubscibeToEvents();
_Cts = new CancellationTokenSource();
_ProcessingEvents = ProcessEvents(_Cts.Token);
return Task.CompletedTask;
}
Task _ProcessingEvents = Task.CompletedTask;
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_Subscriptions?.ForEach(subscription => subscription.Dispose());
_Cts?.Cancel();
try
{
await _ProcessingEvents;
}
catch (OperationCanceledException)
{ }
}
}
}

View File

@ -1,10 +1,7 @@
using Hangfire;
using Hangfire.Common;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire.Annotations;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -21,12 +18,13 @@ using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class InvoiceNotificationManager : IHostedService
{
public static HttpClient _Client = new HttpClient();
HttpClient _Client;
public class ScheduledJob
{
@ -35,128 +33,140 @@ namespace BTCPayServer.HostedServices
get; set;
}
public InvoiceEntity Invoice
public InvoicePaymentNotificationEventWrapper Notification
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
public ILogger Logger
{
get; set;
}
IBackgroundJobClient _JobClient;
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
public InvoiceNotificationManager(
IHttpClientFactory httpClientFactory,
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
EmailSenderFactory emailSenderFactory)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_Client = httpClientFactory.CreateClient();
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
void Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification)
{
var dto = invoice.EntityToDTO(_NetworkProvider);
var notification = new InvoicePaymentNotificationEventWrapper()
{
Data = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
},
Event = new InvoicePaymentNotificationEvent()
{
Code = invoiceEvent.EventCode,
Name = invoiceEvent.Name
},
ExtendedNotification = extendedNotification,
NotificationURL = invoice.NotificationURL
};
// For lightning network payments, paid, confirmed and completed come all at once.
// So despite the event is "paid" or "confirmed" the Status of the invoice is technically complete
// This confuse loggers who think their endpoint get duplicated events
// So here, we just override the status expressed by the notification
if (invoiceEvent.Name == InvoiceEvent.Confirmed)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Confirmed);
}
if (invoiceEvent.Name == InvoiceEvent.PaidInFull)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Paid);
}
//////////////////
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Data.Rate = dto.Rate;
notification.Data.Url = dto.Url;
notification.Data.BTCDue = dto.BTCDue;
notification.Data.BTCPaid = dto.BTCPaid;
notification.Data.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(notification);
_EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail(
invoice.NotificationEmail,
$"BtcPayServer Invoice Notification - ${invoice.StoreId}",
emailBody);
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
if (string.IsNullOrEmpty(invoice.NotificationURL) || !Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute))
return;
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
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);
}
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData)
public async Task NotifyHttp(string invoiceData, CancellationToken cancellationToken)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice);
if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId);
bool reschedule = false;
CancellationTokenSource cts = new CancellationTokenSource(10000);
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
});
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
// 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);
reschedule = true;
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
catch (Exception ex)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
List<string> messages = new List<string>();
@ -166,23 +176,17 @@ namespace BTCPayServer.HostedServices
ex = ex.InnerException;
}
string message = String.Join(',', messages.ToArray());
Logger.LogInformation("Job " + jobId + " threw exception " + message);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = $"Unexpected error: {message}"
});
aggregatorEvent.Error = $"Unexpected error: {message}";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
}
}
@ -199,64 +203,42 @@ namespace BTCPayServer.HostedServices
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
[JsonProperty("extendedNotification")]
public bool ExtendedNotification { get; set; }
[JsonProperty(PropertyName = "notificationURL")]
public string NotificationURL { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellationToken)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = invoice.EntityToDTO(_NetworkProvider);
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
var notificationString = NBitcoin.JsonConverters.Serializer.ToString(notification);
var jobj = JObject.Parse(notificationString);
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
if (notification.ExtendedNotification)
{
#pragma warning disable CS0618
notification.Rate = dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
string notificationString = null;
if (eventCode.HasValue)
{
var wrapper = new InvoicePaymentNotificationEventWrapper();
wrapper.Data = notification;
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
notificationString = JsonConvert.SerializeObject(wrapper);
jobj.Remove("extendedNotification");
jobj.Remove("notificationURL");
notificationString = jobj.ToString();
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
notificationString = jobj["data"].ToString();
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(invoice.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;
}
@ -320,11 +302,6 @@ namespace BTCPayServer.HostedServices
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
{
return $"{invoice.Id}-{invoice.Status}-HTTP";
}
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
@ -341,28 +318,27 @@ namespace BTCPayServer.HostedServices
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.Name == "invoice_expired" ||
e.Name == "invoice_paidInFull" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_markedComplete" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed" ||
e.Name == "invoice_expiredPaidPartial"
if (e.Name == InvoiceEvent.Expired ||
e.Name == InvoiceEvent.PaidInFull ||
e.Name == InvoiceEvent.FailedToConfirm ||
e.Name == InvoiceEvent.MarkedInvalid ||
e.Name == InvoiceEvent.MarkedCompleted ||
e.Name == InvoiceEvent.FailedToConfirm ||
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
tasks.Add(Notify(invoice));
Notify(invoice, e, false);
}
if (e.Name == "invoice_confirmed")
if (e.Name == InvoiceEvent.Confirmed)
{
tasks.Add(Notify(invoice));
Notify(invoice, e, false);
}
if (invoice.ExtendedNotifications)
{
tasks.Add(Notify(invoice, e.EventCode, e.Name));
Notify(invoice, e, true);
}
await Task.WhenAll(tasks.ToArray());
}));

View File

@ -11,7 +11,6 @@ using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
@ -66,10 +65,10 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
invoice.Status = InvoiceStatus.Expired;
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired));
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -84,7 +83,7 @@ namespace BTCPayServer.HostedServices
{
if (invoice.Status == InvoiceStatus.New)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull));
invoice.Status = InvoiceStatus.Paid;
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -93,7 +92,7 @@ namespace BTCPayServer.HostedServices
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration));
context.MarkDirty();
}
}
@ -139,15 +138,15 @@ namespace BTCPayServer.HostedServices
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm));
invoice.Status = InvoiceStatus.Invalid;
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
invoice.Status = InvoiceStatus.Confirmed;
context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed));
context.MarkDirty();
}
}
@ -157,7 +156,7 @@ namespace BTCPayServer.HostedServices
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
invoice.Status = InvoiceStatus.Complete;
context.MarkDirty();
}
@ -247,13 +246,13 @@ namespace BTCPayServer.HostedServices
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
if (b.Name == InvoiceEvent.Created)
{
Watch(b.Invoice.Id);
await Wait(b.Invoice.Id);
}
if (b.Name == "invoice_receivedPayment")
if (b.Name == InvoiceEvent.ReceivedPayment)
{
Watch(b.Invoice.Id);
}
@ -334,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

@ -59,6 +59,18 @@ namespace BTCPayServer.HostedServices
settings.ConvertMultiplierToSpread = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertNetworkFeeProperty)
{
await ConvertNetworkFeeProperty();
settings.ConvertNetworkFeeProperty = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertCrowdfundOldSettings)
{
await ConvertCrowdfundOldSettings();
settings.ConvertCrowdfundOldSettings = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -67,6 +79,44 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertCrowdfundOldSettings()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var app in ctx.Apps.Where(a => a.AppType == "Crowdfund"))
{
var settings = app.GetSettings<Services.Apps.CrowdfundSettings>();
#pragma warning disable CS0618 // Type or member is obsolete
if (settings.UseAllStoreInvoices)
#pragma warning restore CS0618 // Type or member is obsolete
{
app.TagAllInvoices = true;
}
}
await ctx.SaveChangesAsync();
}
}
private async Task ConvertNetworkFeeProperty()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
var blob = store.GetStoreBlob();
#pragma warning disable CS0618 // Type or member is obsolete
if (blob.NetworkFeeDisabled != null)
{
blob.NetworkFeeMode = blob.NetworkFeeDisabled.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always;
blob.NetworkFeeDisabled = null;
store.SetStoreBlob(blob);
}
#pragma warning restore CS0618 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
}
private async Task ConvertMultiplierToSpread()
{
using (var ctx = _DBContextFactory.CreateContext())

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.TryGetValue(cryptoCode, out summary) &&
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
}

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)
{
@ -65,11 +66,10 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSupportedExchanges()
{
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach (var item in tickers
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,14 +36,18 @@ 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;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
using BundlerMinifier.TagHelpers;
namespace BTCPayServer.Hosting
{
@ -58,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 =>
@ -72,6 +78,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
@ -103,17 +110,57 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<AppsHelper>();
services.TryAddSingleton<AppService>();
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
{
var htmlSanitizer = new Ganss.XSS.HtmlSanitizer();
htmlSanitizer.RemovingAtRule += (sender, args) =>
{
};
htmlSanitizer.RemovingTag += (sender, args) =>
{
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase))
{
if (!args.Tag.ClassList.Contains("img-fluid"))
{
args.Tag.ClassList.Add("img-fluid");
}
args.Cancel = true;
}
};
htmlSanitizer.RemovingAttribute += (sender, args) =>
{
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase) &&
args.Attribute.Name.Equals("src", StringComparison.InvariantCultureIgnoreCase) &&
args.Reason == Ganss.XSS.RemoveReason.NotAllowedUrlValue)
{
args.Cancel = true;
}
};
htmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; };
htmlSanitizer.AllowedAttributes.Add("class");
htmlSanitizer.AllowedTags.Add("iframe");
htmlSanitizer.AllowedTags.Remove("img");
htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
htmlSanitizer.AllowedAttributes.Add("allowfullscreen");
return htmlSanitizer;
});
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<PaymentRequestRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100, 1),
Fallback = new FeeRate(100L, 1),
BlockTarget = 20
});
@ -132,6 +179,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<ChangellyClientProvider>();
@ -140,6 +188,11 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
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>();
services.TryAddSingleton<ExplorerClientProvider>();
@ -156,14 +209,16 @@ namespace BTCPayServer.Hosting
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
services.AddTransient<PaymentRequestController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddSingleton<EmailSenderFactory>();
// bundling
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,15 +32,24 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
if (isBitpayAPI && httpContext.Request.Method == "OPTIONS")
{
httpContext.Response.StatusCode = 200;
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
if (httpContext.Request.Headers.ContainsKey("Access-Control-Request-Headers"))
{
httpContext.Response.SetHeader("Access-Control-Allow-Headers", httpContext.Request.Headers["Access-Control-Request-Headers"].FirstOrDefault());
}
return; // We bypass MVC completely
}
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
httpContext.SetBitpayAuth(bitpayAuth);
}
await _Next(httpContext);
@ -79,116 +88,44 @@ 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;
var isCors = method == "OPTIONS";
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
(isCors || (method == "POST" && isJson)))
return true;
if (
bitpayAuth &&
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
(isCors || method == "GET"))
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
(isCors || method == "GET") &&
(isCors || isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
(isCors || method == "GET"))
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
(isCors || method == "GET" || method == "POST"))
return true;
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;
}
}
// Make sure that code executing after this point think that the external url has been hit.
if (_Options.ExternalUrl != null)
{
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
{
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
}
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
{
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
{
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
}
else
{
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
}
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
else
{
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,12 +13,11 @@ 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 Hangfire;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
@ -27,36 +25,22 @@ using BTCPayServer.Controllers;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Mails;
using Microsoft.Extensions.Configuration;
using Hangfire.AspNetCore;
using BTCPayServer.Configuration;
using System.IO;
using Hangfire.Dashboard;
using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Hosting
{
public class Startup
{
class NeedRole : IDashboardAuthorizationFilter
{
string _Role;
public NeedRole(string role)
{
_Role = role;
}
public bool Authorize([NotNull] DashboardContext context)
{
return context.GetHttpContext().User.IsInRole(_Role);
}
}
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Configuration = conf;
@ -78,7 +62,7 @@ namespace BTCPayServer.Hosting
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -99,7 +83,7 @@ namespace BTCPayServer.Hosting
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 7;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
@ -107,21 +91,6 @@ namespace BTCPayServer.Hosting
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
@ -188,15 +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.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
app.UseSignalR(route =>
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
AppHub.Register(route);
PaymentRequestHub.Register(route);
});
app.UseWebSockets();
app.UseStatusCodePages();

33
BTCPayServer/IDelay.cs Normal file
View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public interface IDelay
{
Task Wait(TimeSpan delay, CancellationToken cancellationToken);
}
public class TaskDelay : IDelay
{
TaskDelay()
{
}
private static readonly TaskDelay _Instance = new TaskDelay();
public static TaskDelay Instance
{
get
{
return _Instance;
}
}
public Task Wait(TimeSpan delay, CancellationToken cancellationToken)
{
return Task.Delay(delay, cancellationToken);
}
}
}

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

View File

@ -0,0 +1,47 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AddPaymentRequests : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PaymentRequests",
columns: table => new
{
Id = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: true),
Status = table.Column<int>(nullable: false),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentRequests", x => x.Id);
table.ForeignKey(
name: "FK_PaymentRequests_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PaymentRequests_Status",
table: "PaymentRequests",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_PaymentRequests_StoreDataId",
table: "PaymentRequests",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentRequests");
}
}
}

View File

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

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AppsTagging : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "TagAllInvoices",
table: "Apps",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TagAllInvoices",
table: "Apps");
}
}
}

View File

@ -14,7 +14,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
.HasAnnotation("ProductVersion", "2.1.8-servicing-32085");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -63,6 +63,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId");
b.Property<bool>("TagAllInvoices");
b.HasKey("Id");
b.HasIndex("StoreDataId");
@ -326,6 +328,26 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@ -526,6 +548,14 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")

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

@ -0,0 +1,96 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.AppViewModels
{
public class UpdateCrowdfundViewModel
{
[Required] [MaxLength(30)] public string Title { get; set; }
[MaxLength(50)] public string Tagline { get; set; }
[Required] public string Description { get; set; }
[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)")]
public bool Enabled { get; set; } = false;
[Required]
[Display(Name = "Enable background animations on new payments")]
public bool AnimationsEnabled { get; set; } = true;
[Required]
[Display(Name = "Enable sounds on new payments")]
public bool SoundsEnabled { get; set; } = true;
[Required]
[Display(Name = "Enable Disqus Comments")]
public bool DisqusEnabled { get; set; } = true;
[Display(Name = "Disqus Shortname")] public string DisqusShortname { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
[Required]
[MaxLength(5)]
[Display(Name = "Primary currency used for targets and stats. (e.g. BTC, LTC, USD, etc.)")]
public string TargetCurrency { get; set; } = "BTC";
[Display(Name = "Set a Target amount ")]
[Range(0, double.PositiveInfinity)]
public decimal? TargetAmount { get; set; }
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
public int ResetEveryAmount { get; set; } = 1;
[Display(Name = "Do not allow additional contributions after target has been reached")]
public bool EnforceTargetAmount { get; set; }
[Display(Name = "Contribution Perks Template")]
public string PerksTemplate { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
public bool UseAllStoreInvoices { get; set; }
public string AppId { get; set; }
public string SearchTerm { get; set; }
[Display(Name = "Sort contribution perks by popularity")]
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,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.AppViewModels
{
@ -14,17 +11,27 @@ namespace BTCPayServer.Models.AppViewModels
[Required]
[MaxLength(5)]
public string Currency { get; set; }
[MaxLength(5000)]
public string Template { get; set; }
[Display(Name = "Enable shopping cart")]
public bool EnableShoppingCart { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
[Display(Name = "User can input discount in %")]
public bool ShowDiscount { get; set; }
[Display(Name = "Enable tips")]
public bool EnableTips { get; set; }
public string Example1 { get; internal set; }
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)]
@ -36,11 +43,16 @@ namespace BTCPayServer.Models.AppViewModels
public string CustomButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Do you want to leave a tip?")]
[Display(Name = "Text to display in the tip input")]
public string CustomTipText { get; set; }
[MaxLength(30)]
[Display(Name = "Tip percentage amounts (comma separated)")]
public string CustomTipPercentages { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
public string Id { get; set; }
}
}

View File

@ -0,0 +1,89 @@
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; }
public string Title { get; set; }
public string Description { get; set; }
public string MainImageUrl { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string TargetCurrency { get; set; }
public decimal? TargetAmount { get; set; }
public bool EnforceTargetAmount { get; set; }
public CrowdfundInfo Info { get; set; }
public string Tagline { get; set; }
public ViewPointOfSaleViewModel.Item[] Perks { get; set; }
public bool DisqusEnabled { get; set; }
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; }
public Dictionary<string, int> PerkCount { get; set; }
public CurrencyData CurrencyData { get; set; }
public class CrowdfundInfo
{
public int TotalContributors { get; set; }
public decimal CurrentPendingAmount { get; set; }
public decimal CurrentAmount { get; set; }
public decimal? ProgressPercentage { get; set; }
public decimal? PendingProgressPercentage { get; set; }
public DateTime LastUpdated { get; set; }
public Dictionary<string, decimal> PaymentStats { get; set; }
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
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
{
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
[Required] public decimal Amount { get; set; }
public string Email { get; set; }
public string ChoiceKey { get; set; }
public bool RedirectToCheckout { get; set; }
public string RedirectUrl { get; set; }
}
}

View File

@ -28,13 +28,16 @@ namespace BTCPayServer.Models.AppViewModels
public string CurrencySymbol { get; set; }
public string ThousandSeparator { get; set; }
public string DecimalSeparator { get; set; }
public int Divisibility { get; internal set; }
public int Divisibility { get; set; }
public bool SymbolSpace { get; set; }
}
public CurrencyInfoData CurrencyInfo { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }
@ -44,6 +47,7 @@ namespace BTCPayServer.Models.AppViewModels
public string ButtonText { get; set; }
public string CustomButtonText { get; set; }
public string CustomTipText { get; set; }
public int[] CustomTipPercentages { get; set; }
public string CustomCSSLink { get; set; }
}

View File

@ -7,6 +7,15 @@ namespace BTCPayServer.Models
{
public class ConfirmModel
{
public ConfirmModel() { }
public ConfirmModel(string title, string desc, string action = null)
{
Title = title;
Description = desc;
Action = action;
}
public string Title
{
get; set;

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
public class CreateInvoiceRequest
{
[JsonProperty(PropertyName = "buyer")]
public Buyer Buyer { get; set; }
[JsonProperty(PropertyName = "buyerEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerEmail { get; set; }
[JsonProperty(PropertyName = "buyerCountry", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerCountry { get; set; }
[JsonProperty(PropertyName = "buyerZip", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerZip { get; set; }
[JsonProperty(PropertyName = "buyerState", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerState { get; set; }
[JsonProperty(PropertyName = "buyerCity", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerCity { get; set; }
[JsonProperty(PropertyName = "buyerAddress2", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerAddress2 { get; set; }
[JsonProperty(PropertyName = "buyerAddress1", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerAddress1 { get; set; }
[JsonProperty(PropertyName = "buyerName", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerName { get; set; }
[JsonProperty(PropertyName = "physical", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Physical { get; set; }
[JsonProperty(PropertyName = "redirectURL", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string RedirectURL { get; set; }
[JsonProperty(PropertyName = "notificationURL", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string NotificationURL { get; set; }
[JsonProperty(PropertyName = "extendedNotifications", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool ExtendedNotifications { get; set; }
[JsonProperty(PropertyName = "fullNotifications", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool FullNotifications { get; set; }
[JsonProperty(PropertyName = "transactionSpeed", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string TransactionSpeed { get; set; }
[JsonProperty(PropertyName = "buyerPhone", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerPhone { get; set; }
[JsonProperty(PropertyName = "posData", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string PosData { get; set; }
[JsonProperty(PropertyName = "itemCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemCode { get; set; }
[JsonProperty(PropertyName = "itemDesc", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemDesc { get; set; }
[JsonProperty(PropertyName = "orderId", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string OrderId { get; set; }
[JsonProperty(PropertyName = "currency", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Currency { get; set; }
[JsonProperty(PropertyName = "price", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal Price { get; set; }
[JsonProperty(PropertyName = "notificationEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string NotificationEmail { get; set; }
[JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty(PropertyName = "expirationTime", DefaultValueHandling = DefaultValueHandling.Ignore)]
public DateTimeOffset? ExpirationTime { get; set; }
[JsonProperty(PropertyName = "status", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Status { get; set; }
[JsonProperty(PropertyName = "minerFees", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, MinerFeeInfo> MinerFees { get; set; }
[JsonProperty(PropertyName = "supportedTransactionCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
[JsonProperty(PropertyName = "exchangeRates", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }
[JsonProperty(PropertyName = "refundable", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Refundable { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal? TaxIncluded { get; set; }
[JsonProperty(PropertyName = "nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
public long Nonce { get; set; }
[JsonProperty(PropertyName = "guid", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Guid { get; set; }
[JsonProperty(PropertyName = "token", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Token { get; set; }
}
}

View File

@ -40,6 +40,12 @@ namespace BTCPayServer.Models
//{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse
{
[JsonIgnore]
public string StoreId
{
get; set;
}
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
public string Url
@ -84,6 +90,12 @@ namespace BTCPayServer.Models
get; set;
}
[JsonProperty("taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
//"currency":"USD"
[JsonProperty("currency")]
public string Currency

View File

@ -105,6 +105,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TaxIncluded { get; set; }
public BuyerInformation BuyerInformation
{
get;
@ -143,6 +144,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, string> PosData { get; set; }
public Dictionary<string, object> PosData { get; set; }
}
}

View File

@ -15,6 +15,10 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public int Total
{
get; set;
}
public string SearchTerm
{
get; set;

View File

@ -20,11 +20,11 @@ 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; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
public string BtcDue { get; set; }
@ -61,5 +61,11 @@ namespace BTCPayServer.Models.InvoicingModels
public string PeerInfo { get; set; }
public string ChangellyMerchantId { get; set; }
public decimal? ChangellyAmountDue { get; set; }
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; }
}
}

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