Compare commits

...

289 Commits

Author SHA1 Message Date
dfe655393d Fix enable payjoin when p2sh 2020-04-16 17:23:29 +02:00
8c81dae167 bump 2020-04-16 22:44:53 +09:00
de75d30f06 bump 2020-04-16 22:41:36 +09:00
8ba99d4e7c Merge pull request from NicolasDorier/changelog
Add changelog 1.0.4.0
2020-04-16 22:40:04 +09:00
c3bc25a7d4 Add changelog 2020-04-16 22:38:38 +09:00
42be03b560 bump nbx and nbitcoin 2020-04-16 19:37:10 +09:00
e73aece9c3 Merge pull request from dennisreimann/404-center-supporters
Center supporter logos on 404 page
2020-04-16 14:47:35 +09:00
69c57867b3 Merge pull request from NicolasDorier/persisit/offchaintx
Persist offchain transactions
2020-04-16 14:46:48 +09:00
7434163848 Persist offchain transactions 2020-04-16 14:36:50 +09:00
00d1c4ebcc Navbar fixes ()
* Fix onion position in mobile navbar

* Show nav hover highlight only on desktop

* Fix navbar icon color
2020-04-15 17:13:58 +02:00
26067fbfe2 Center supporter logos on 404 page 2020-04-15 16:49:13 +02:00
03458efea4 Change payjoin endpoint key to pj 2020-04-13 11:52:22 +02:00
bd21bf9c0f add payjoin tooltip 2020-04-13 11:51:25 +02:00
5b7a20c33e Merge pull request from NicolasDorier/payjoin/persistance
Persist planned transactions and locks for payjoin
2020-04-13 16:57:13 +09:00
c73c34dfaa Persisting locked input and outpoints 2020-04-13 16:06:07 +09:00
a3a9361ba5 Persist the Delayed Broadcaster 2020-04-13 16:00:33 +09:00
2f0e9569a1 Fix build error 2020-04-13 15:57:57 +09:00
511a0efa89 Merge pull request from Kukks/hot-wallet-policy
add additional server policy for hot wallet RPC import
2020-04-13 15:51:01 +09:00
4ae91ba307 Merge pull request from bolatovumar/fix-1459
Make sure sponsor logos show up nicely on all screen sizes
2020-04-13 15:49:23 +09:00
3a70f467eb Merge pull request from bolatovumar/fix-1456
Add mnemonic phrase color CSS variable
2020-04-13 15:48:55 +09:00
4e09bb0b01 add additional server policy for hot wallet
So that if you enable hot wallets for all, you can still not allow them to import to your RPC
2020-04-13 08:48:35 +02:00
5ae18cf21f Make sure mnemonic phrase doesn't blend into background 2020-04-13 08:05:44 +02:00
6bfb6a795e Merge pull request from NicolasDorier/improve-ux-hotwallet
Do not ask for address confirmation if a new wallet is generated
2020-04-13 13:55:34 +09:00
1d2540543b Payjoin: Randomly round the payment output if it is one input tx 2020-04-13 13:34:23 +09:00
9efe6267d3 Do not ask for address confirmation if a new wallet is generated 2020-04-13 13:03:55 +09:00
cb10551d2c Make sure sponsor logos show up nicely on all screen sizes
fix 
2020-04-12 15:08:28 -07:00
b0073af5aa Merge pull request from Kukks/api/api-key-uu-refactor
Refactor UI for Add APIKey/Authorize
2020-04-10 22:00:32 +09:00
7ca7f53446 Merge pull request from dennisreimann/modern-theme
New default theme
2020-04-10 16:55:55 +09:00
ad284a4b61 Refactor UI for Add APIKey/Authorize 2020-04-10 09:49:01 +02:00
95e7d5ded9 fix tests 2020-04-10 09:18:08 +02:00
55722b3191 Improve border color themeing 2020-04-10 09:18:08 +02:00
5e34efc9f4 Improve invoice table details display 2020-04-10 09:18:08 +02:00
6f85ffd9df Update footer color 2020-04-10 09:18:08 +02:00
9783a76c38 Fix generated API key color 2020-04-10 09:18:08 +02:00
05952f95f1 Split light and dark theme
To make them explicitely selectable.
2020-04-10 09:18:08 +02:00
b9c97cc5d7 Homepage updates 2020-04-10 09:18:08 +02:00
d5b088b924 Improve color handling 2020-04-10 09:18:08 +02:00
fbd5673cfd POS logo coloring fix 2020-04-10 09:18:08 +02:00
dbb7ad083a Add alert-link classes where necessary 2020-04-10 09:18:08 +02:00
ce7e4234cc Decrease font-size to match classic theme 2020-04-10 09:18:07 +02:00
d2b38fdfce Set default theme for kitchensink 2020-04-10 09:18:07 +02:00
fcdcc5e69b Update main nav hover
fix
2020-04-10 09:18:07 +02:00
8d73606809 Footer updates 2020-04-10 09:18:07 +02:00
15a7c4d092 Use consistent variable names 2020-04-10 09:18:07 +02:00
c205e41072 Rename modern to default theme 2020-04-10 09:18:07 +02:00
48c220b751 Header updates for light/dark theme
fix
2020-04-10 09:18:07 +02:00
06ff268644 Update bootstrap and kitchensink
Discourage use of explicit light/dark classes, because they interfere with the approach of having parallel light/dark themes.
2020-04-10 09:18:07 +02:00
9ee920a816 Draft modern theme
Based on the design input from Figma, see https://www.figma.com/file/C7Xyq0FlxgFW8vaBr8ht1z/BTCPAY?node-id=17%3A126

Color updates
2020-04-10 09:18:07 +02:00
a403363015 Move font-size setting to site.css 2020-04-10 09:18:07 +02:00
6274958409 Trust the payment method details for the payjoin enabled and make invoice logs consistent () 2020-04-10 16:00:41 +09:00
d47e225dce Fix email sending on registration crash () 2020-04-10 15:59:39 +09:00
841cf61c92 Add L-CAD support (new Liquid Asset) ()
* Add Liquid CAD support

Adds the Liquid CAD asset ID from Bull Bitcoin

* Add the Liquid CAD logo

* Fix image asset

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2020-04-10 15:59:02 +09:00
2d2c5b46af Support RBF and PayJoin for GRS () 2020-04-10 15:38:00 +09:00
cc80e4636f Remove runtime compile on tests () 2020-04-09 23:19:45 +09:00
bb24c95e71 delete files on user delete ()
fixes 
(I did not do a cascade delete for 2 reasons: Sqlite does not support the migration easily to alter a foreign key AND files would be orphaned in the storage with no record in the db)
2020-04-09 22:17:06 +09:00
c7a4158a39 use payjoin key from constants 2020-04-09 14:07:26 +02:00
ed0e423aa7 Add payment link in checkout with payjoin indicator ()
* Add payment link in checkout with payjoin indicator

* move to bottom
2020-04-09 20:44:08 +09:00
1c0d713b00 Catch all error thrown by EndAccept 2020-04-09 20:36:19 +09:00
70c80f4d44 Remove useless line 2020-04-09 20:27:24 +09:00
e3f6de8472 Remove warnings, replace BIP79 by Payjoin, Fix strange error in Accept loop 2020-04-09 20:25:17 +09:00
95644f8884 Fix tests 2020-04-09 19:44:16 +09:00
f57db12c09 Document downside of socks proxy 2020-04-09 18:56:30 +09:00
0d821ff4db Connection: close when using the internal proxy, fix gateway errors with connections in limbo 2020-04-09 18:49:30 +09:00
6927d81175 Implement Http Tor proxy as a internal http proxy 2020-04-09 17:38:55 +09:00
e183714475 Add Tor Http Client Factory ()
* WIP: Add Tor Http Client Factory

This PR adds a Tor Http Client Factory so that we can do HTTP requests over socks5( which we usually use tor's socks5).
Using it for payjoin when possible.

Currently have an issue where a 503 is always returned.  Must be something wrong with my tor config or the socks proxy lib Im using

* fix

* remove external dependency

* make payjoin client use tor client if available

* fix docker test

* use tor client only if available and necessary

* remove bip 79 mention
2020-04-08 22:40:41 +09:00
6602823067 Refactor the input type detection 2020-04-08 22:14:16 +09:00
111feeb673 Merge pull request from Kukks/payjoin/p2sh
Payjoin: P2SH support
2020-04-08 22:00:07 +09:00
3bf1b78b33 fix 2020-04-08 14:32:29 +02:00
751ccc333f make test less slimfy 2020-04-08 14:32:29 +02:00
624e6e4744 adapt 2020-04-08 14:32:29 +02:00
73b13c750d add P2sh support 2020-04-08 14:32:29 +02:00
148b04e9ba allow changing the payjoin key easily later 2020-04-08 14:32:29 +02:00
32938479ac WIP: Payjoin: P2SH support 2020-04-08 14:32:29 +02:00
aae0086e68 Merge pull request from dennisreimann/wallet-view-improvements
Wallet view improvements
2020-04-08 21:25:31 +09:00
1f4556bd9d Merge pull request from dennisreimann/transactions-dropdowns
Fix transaction dropdowns
2020-04-08 21:24:23 +09:00
d7bb15cac3 Merge pull request from dennisreimann/docker-bitcoin-generate-shell-script
Add bitcoin generate shell script
2020-04-08 21:22:47 +09:00
9a54445785 Merge pull request from Kukks/monero-fix
monero fixes
2020-04-08 21:11:27 +09:00
523edfef58 Fix wording for no vault connection 2020-04-08 13:29:54 +02:00
5a841216b8 Add checkbox coin selection (js version) 2020-04-08 13:29:16 +02:00
d6d58a98db Improve non-js coin selection 2020-04-08 13:15:03 +02:00
6a0cda69c9 Wallet view improvements
As brought up by @kukks with [this comment](https://github.com/btcpayserver/btcpayserver/pull/1434#issuecomment-609724703). These improvements can be applied across themes, so these fixes are separated from the originating PR.
Pure visual finetunings for the Coin Selection form and amount/fee rate links.
2020-04-08 12:38:26 +02:00
24691e5290 Bump NBitcoin and NBX 2020-04-08 19:34:12 +09:00
b203d369fb Fix payjoin tests 2020-04-08 18:24:04 +09:00
b1cc30d25d Fix sequence check on client 2020-04-08 17:51:22 +09:00
72e64885be Fix sequence check on client 2020-04-08 17:42:50 +09:00
f4a47f5197 Deterministically sort the UTXOs to select 2020-04-08 13:46:11 +09:00
fe45152529 Add bitcoin generate shell script
The shell equivalent to the existing powershell script.
2020-04-07 18:13:47 +02:00
9a9773853e Fix bug in payjoin receiver where too much fee could be substracted from receiver 2020-04-07 20:04:08 +09:00
9d2ab8b154 The payjoin receiver can inject a fake change 2020-04-07 18:14:31 +09:00
ba2184e21a Do not remove outputs in payjoin tx 2020-04-07 15:10:19 +09:00
829b0dd5e2 Fix transaction dropdowns
As brought up by @kukks with [this comment](https://github.com/btcpayserver/btcpayserver/pull/1434#issuecomment-609720019). The problem exists across themes, so the fix is separated from the originating PR.
2020-04-06 18:16:41 +02:00
9fc451c9ca Make stronger check on fee, let the merchant make bigger transaction if he pays the fee 2020-04-06 23:24:51 +09:00
452568e740 monero fixes 2020-04-06 16:12:48 +02:00
5503132ffc Fix vue-i18n var substitution error in checkout
fixes 
2020-04-06 13:38:58 +02:00
17a6b7d34f Rename an error of pj to unsupported-input 2020-04-06 20:27:48 +09:00
69ad9edc9a Check pj with 1 sat/b 2020-04-06 20:26:50 +09:00
80bb959ac3 Merge pull request from Kukks/wallet/camera
Scan address/bip21 with camera
2020-04-06 20:07:32 +09:00
1e0587af26 Make sure tests use fixed fees 2020-04-06 19:18:49 +09:00
16e35e8b55 update html styles 2020-04-06 11:51:01 +02:00
8278926e42 change wallet send buttons 2020-04-06 11:51:01 +02:00
1debbc3cdb Change error messages 2020-04-06 11:51:01 +02:00
65c99ead1d cleanup 2020-04-06 11:51:00 +02:00
2693dacae6 Scan address/bip21 with camera 2020-04-06 11:51:00 +02:00
7ce614f1c4 Merge pull request from btcpayserver/pr/dark-checkout
Dark checkout theme
2020-04-06 17:46:17 +09:00
79a0f97abb Merge pull request from Kukks/api/permission-blob
GreenField: Switch to Blob for API Keys
2020-04-06 17:42:32 +09:00
670e0ee7df Merge pull request from NicolasDorier/remove-rider-errors
Remove bugs reported by Rider
2020-04-06 17:36:07 +09:00
3f231a8894 Merge pull request from dennisreimann/manage-store-spacings
Improve section spacings on manage store page
2020-04-06 17:35:05 +09:00
f5dfee7642 Improve section spacings on manage store page 2020-04-06 10:12:05 +02:00
ab120c5dcb Fix tests 2020-04-06 17:05:03 +09:00
f085a5618b Defaulting Powered by BtcPayServer link to green in light and dark theme 2020-04-05 23:21:14 -05:00
d939baac84 Give more information to the user if payjoin fails 2020-04-06 13:19:51 +09:00
41d70e8462 Tweaking black background on Pay with and Totals line, colors for dropdowns 2020-04-05 23:09:31 -05:00
9af7edf8b8 Add comments to explain the Payment's ReceivedTime 2020-04-06 12:23:56 +09:00
01a8c20ee8 Use better time precision for time in PaymentEntity 2020-04-05 23:57:43 +09:00
4c966e2a09 Refactor the detection of the payjoin payment 2020-04-05 22:44:34 +09:00
42aead3c89 A replacement payment should have the same fee as the replaced one 2020-04-05 20:48:00 +09:00
a348960041 Make CanChangeNetworkFeeMode more resilient 2020-04-05 20:00:28 +09:00
c737a25234 Add logs to test CanChangeNetworkFeeMode 2020-04-05 18:54:12 +09:00
a01b2e4a83 Small optimization for faster AddDerivationScheme 2020-04-05 18:35:29 +09:00
5f838db281 Merge pull request from Kukks/api/merge-multiple-swagger
Swagger Generator: Merge multiple documents
2020-04-05 17:36:04 +09:00
08beffb005 Make sure FindPaymentViaPolling does not requests over and over the same coins 2020-04-05 16:50:19 +09:00
76b919d887 make swagger test use live endpoint 2020-04-05 09:43:49 +02:00
c106ac2c42 Adding margin around QR code 2020-04-04 23:44:47 -05:00
4a1fb71e09 Do not print dangerous info in the logs 2020-04-05 12:33:10 +09:00
98e2baae19 Remove bugs reported by Rider 2020-04-05 12:30:23 +09:00
963c69a0e0 Merge pull request from Kukks/bpu
BIP79
2020-04-05 12:10:55 +09:00
fd026a9733 Refactor server-side 2020-04-05 12:02:36 +09:00
e76785a64e Removing background gradient
It's not smooth on most displays
2020-04-04 18:03:37 -05:00
1a8f222e46 Migrating fixes on default theme back to legacy style 2020-04-04 17:54:13 -05:00
24d26d7a44 Streamlining btnGroupLnd styling 2020-04-04 17:49:43 -05:00
c86370c25a Extracting css class for Powered by BtcPayServer 2020-04-04 17:28:00 -05:00
20cba1d3a1 Tweaking colors on language dropdown hover 2020-04-04 17:16:09 -05:00
3e2efc7f27 Adding first iteration of dark design 2020-04-04 16:47:37 -05:00
d2c29aaec6 Tweaking text opacity, recommended fee line consistency 2020-04-04 16:47:26 -05:00
bb1c5dead5 Moving selection slider to bottom to comply with new design 2020-04-04 16:32:44 -05:00
41cc79600a Swagger Generator: Merge multiple documents
It's becoming very hard to edit the swagger file as it grows (especially with multiple PRs altering it). This PR allows the swagger file to be generated from multiple jsons instead which are merged in the controller.
2020-04-04 14:22:07 +02:00
238d4fceea Merge pull request from Kukks/fix-dead-link-tester
Fix dead link tester
2020-04-04 14:21:22 +02:00
c6d75de3d7 GreenField: Switch to Blob for API Keys 2020-04-02 09:32:22 +02:00
9e1ae29600 change broadcast button style 2020-03-31 14:57:24 +02:00
d60b00e8cd Merge pull request from Kukks/coin-selection-fix
Fix coin selection filter bug
2020-03-31 18:43:41 +09:00
49786f4195 add bitpay to blacklist 2020-03-31 11:13:22 +02:00
7b6eae6053 Fixing CheckNoDeadLink test now that btse blocks our call from circleci 2020-03-31 11:12:01 +02:00
a408541eb3 Fix coin selection filter bug 2020-03-31 11:07:57 +02:00
1ba25448cc Merge pull request from btcpayserver/feat/new-invoice
New invoice design
2020-03-31 17:33:10 +09:00
4d2e59e1a1 Change flow of payjoin in wallet + fix tests 2020-03-30 13:27:04 +02:00
7b4f686add Moving default theme back to root now that we don't have separate setting 2020-03-30 01:38:45 -05:00
ee0ef2881a Removing extra setting, depending on Custom CSS for bundled themes 2020-03-28 14:10:18 -06:00
22f79e9fe4 Merge pull request from Kukks/wallet-receive-improve
Allow wallet receive to generate new addresses
2020-03-29 00:38:32 +09:00
fdad5a47d5 Reformating file according to project standards 2020-03-27 18:04:30 -05:00
e32f3cbf80 Preserving legacy margin for previous CustomLogoLinks 2020-03-27 18:04:05 -05:00
b56d026fdb small changes 2020-03-27 14:59:00 +01:00
64717328f6 check output sum of proposed payjoin 2020-03-27 10:45:13 +01:00
065be9be64 Revert back to original transaction if payjoined tx does not work 2020-03-27 10:45:13 +01:00
10fcfab233 remove bpu TempData 2020-03-27 10:45:13 +01:00
ff9865c516 fix tor docker 2020-03-27 10:45:13 +01:00
59bae2c337 ToHashSet 2020-03-27 10:45:13 +01:00
89d9793692 remove support from old ledger websocket 2020-03-27 10:45:13 +01:00
23b2f55b47 use nameof 2020-03-27 10:45:13 +01:00
886510c2e1 remove tor for now 2020-03-27 10:45:13 +01:00
2b11b43d6d NRE fix 2020-03-27 10:45:13 +01:00
d90ffb2254 move payjoin settings to store settings from checkout experience 2020-03-27 10:45:13 +01:00
fc88a867fa try fix test 2020-03-27 10:45:13 +01:00
e4cb1a875b Remove tor client factory 2020-03-27 10:45:13 +01:00
1a62ee9260 try fix tor socks5 connection 2020-03-27 10:45:12 +01:00
56d5e6f99f fixes 2020-03-27 10:45:12 +01:00
f1821636db fix tor client creator 2020-03-27 10:45:12 +01:00
2e3a0706ee RBF Protection & Handling 2020-03-27 10:45:12 +01:00
89da4184ff BIP79 Support 2020-03-27 10:45:12 +01:00
1895e154d9 Add missing file 2020-03-27 18:42:59 +09:00
6d7b57ea3b A api key can always revoke itself, add a route to delete any api key 2020-03-27 14:46:51 +09:00
39a8c3fe47 GreenField: Create API Key 2020-03-27 14:17:31 +09:00
927c09ff7b Merge pull request from NicolasDorier/refactor/greenfield-classes
Rename classes in Greenfield Authentication, use different AuthenticationScheme for Basic versus APIKey
2020-03-27 13:41:32 +09:00
08abda1522 Restrict authentication to the APIController to GreenFieldAPIKeys 2020-03-27 13:34:03 +09:00
d219ba5d32 Split the greenfield authhandler in two classes 2020-03-27 13:06:41 +09:00
afdee9d8a2 Move directories, rename controllers 2020-03-27 12:58:45 +09:00
ac14f199e4 Renaming GreenField classes 2020-03-27 12:55:21 +09:00
76818fa385 Rename API Keys folder to GreenField 2020-03-27 12:44:21 +09:00
49be370e51 Rename GreenField auth to APIKey auth in swagger doc 2020-03-27 12:43:06 +09:00
fbe89f1784 Merge pull request from pavlenex/invoice-summary
Rename Paid summary to Invoice Summary
2020-03-27 12:32:15 +09:00
b7afcb90a2 Tweaking checkout logo to be set from css 2020-03-26 19:49:04 -05:00
a6ac67963e Adding setting to CheckoutExperience to pick checkout theme 2020-03-26 19:49:00 -05:00
bde8ed7aa2 Extracting checkout theme css as separate link 2020-03-26 19:48:54 -05:00
ca234838a3 Tweaks for close button, recommended fee 2020-03-26 19:48:48 -05:00
d54d340bef Adjusting new color scheme on invoice 2020-03-26 19:48:42 -05:00
a926a5eedf Fix warning 2020-03-26 22:56:30 +09:00
0df5e7d7a3 Fix clone button in edit payment request (Fix ) 2020-03-26 22:54:47 +09:00
034fb4ec80 Rename Paid summary to Invoice Summary 2020-03-26 12:50:08 +01:00
69482eb4fb Allow wallet receive to generate new addresses 2020-03-25 17:24:41 +01:00
10e52f08be Prevent NRE exception 2020-03-26 01:21:47 +09:00
5565d8dae5 Catch error with bitflyer 2020-03-26 01:21:46 +09:00
c633402fe2 Merge pull request from Kukks/coin-selection-fix
fix coin selection
2020-03-26 01:08:53 +09:00
0688feea3c Fix docker files 2020-03-26 00:57:54 +09:00
c906fd42df Add bitflyer direct integration 2020-03-26 00:48:01 +09:00
6468b39121 try fix test again 2020-03-25 14:11:38 +01:00
d0a95f5a69 fix coin selection 2020-03-25 14:11:38 +01:00
e36338d903 Merge pull request from Kukks/basic-auth
Greenfield API: Basic Auth
2020-03-25 21:00:14 +09:00
e596513fc1 Merge pull request from Kukks/fix-link-test
Fix Link checker test
2020-03-25 19:33:50 +09:00
77588182b9 Make validation uniform 2020-03-24 23:44:26 -05:00
ca00caa4a4 Do not allow 0 amount invocies for crowdfund and payment requests 2020-03-24 23:43:56 -05:00
36bd76248b Allow Pay Button to work on Apps
This PR allows you to use the pay button generator to create buttons that target apps. This means that you can generate an invoice that is linked to an item on the POS/Crowdfund (targeting the item is optional). The POS/Crowdfund item amount -> invoice creation amount validation works too so that the user cannot modify the amount of a perk using just html ( fixes  )
2020-03-24 23:43:47 -05:00
f0f05acdfd Let 0 amount invoices pass through and allow email to be set when required but paid
@ketominer @Askuwheteau @IAskuwheteau
2020-03-24 23:43:19 -05:00
6df7ffd7e2 lol windows phone 2020-03-24 08:55:47 +01:00
91924512e6 Fix Link checker test 2020-03-24 08:00:47 +01:00
7899c2d5c5 fix test 2020-03-23 21:18:40 +01:00
56ba834ca2 Consolidate auth into one 2020-03-23 16:46:49 +01:00
d57fdd4785 Remove useless delay in tests 2020-03-23 17:19:19 +09:00
805e1f53b3 Test Wallet Receive, Send with NBX, Coin selection 2020-03-23 15:46:54 +09:00
40953ef2c6 Merge pull request from Kukks/coin-selection
New feature: Coin Selection
2020-03-23 14:05:11 +09:00
ff055c08fb Merge pull request from btcpayserver/pr/can-manage-wallet
Additional logging and tweaking for CanManageWallet test
2020-03-22 13:53:29 +09:00
f3d5cf3622 Additional logging and tweaking for CanManageWallet test 2020-03-21 12:23:02 -05:00
e48e8c34d9 fix tst 2020-03-20 17:59:14 +01:00
98a48cd0a5 fix swagger validation test 2020-03-20 17:37:39 +01:00
f8f358ebdb add to client, fix tests and doc 2020-03-20 17:14:47 +01:00
9d99c32305 add basic auth for greenfield 2020-03-20 14:07:31 +01:00
478b1463ff Improve documentation 2020-03-20 20:26:36 +09:00
7e7f0053e2 Improve documentation 2020-03-20 20:25:10 +09:00
9a940a044e Fix tests 2020-03-20 20:00:30 +09:00
d2864ccd7c Make sure ApiKeyData set all the fields, remove UserId 2020-03-20 20:00:05 +09:00
ad4dbdad6d Fix the PermissionJsonConverter 2020-03-20 19:57:00 +09:00
094307d688 Remove warning and improve UI of permission selection 2020-03-20 19:39:02 +09:00
53e7c84e73 Fix tests 2020-03-20 18:56:30 +09:00
2a865284da Merge pull request from Kukks/liquid-assets-divisibility
Fix liquid asset BIP21 decimal precision
2020-03-20 18:39:05 +09:00
4666238e38 Fix build 2020-03-20 18:38:21 +09:00
b54a7b80e3 add tests and fix 2020-03-20 09:31:22 +01:00
432d6bb261 Update documentation 2020-03-20 14:33:11 +09:00
fb36ed2cae Fix tests 2020-03-20 14:07:42 +09:00
55516a3253 Remove excessive folders 2020-03-20 14:03:28 +09:00
a0e638d500 Switch from System.Text.Json to Newtonsoft, typify the BTCPayServer.Client 2020-03-20 14:01:51 +09:00
2def9e7bd3 fix build 2020-03-20 13:58:07 +09:00
0bfc12ae3d Fix build 2020-03-20 13:44:02 +09:00
318d826694 Rename Permissions.Can.. to Policies.Can.. 2020-03-20 13:41:47 +09:00
44b3bb34a4 Merge pull request from NicolasDorier/refactor/permissions
Refactor permissions of GreenField
2020-03-20 13:30:58 +09:00
46edc281b6 Fix tests 2020-03-20 13:22:10 +09:00
d72139c2c1 Fix liquid asset BIP21 decimal precision
The way liquid assets decimal precision works is just an ui layer. Each unit of an asset is actually 1sat.
Fixes https://github.com/Blockstream/green_android/issues/86
2020-03-19 16:00:33 +01:00
29a807696b Refactor permissions of GreenField 2020-03-19 23:43:51 +09:00
517c65f1fc make no js version a littler better 2020-03-19 10:08:33 +01:00
8f18be727b add clear option 2020-03-19 10:02:31 +01:00
d6c66d0c03 New feature: Coin Selection
This opt-in feature allows you to select which utxos you want to use for a specific transaction.
2020-03-19 09:44:47 +01:00
eac33d494a Add logs 2020-03-19 13:34:11 +09:00
2105b44610 Make sure the create user is respecting the disable-registration settings 2020-03-19 13:30:53 +09:00
ab74013a05 Merge pull request from btcpayserver/pr/fix-tests
Adding loggin in CanManageWallet test, fixing UserControllerTests
2020-03-19 13:15:17 +09:00
967b02e373 Commenting out conflicting check 2020-03-18 19:17:15 -05:00
8432cd5477 Adding new check for expected behavior in UsersControllerTests 2020-03-18 19:01:27 -05:00
ccfca65c41 Reverting changes to UsersController because of CanCreateUsersViaAPI test 2020-03-18 18:55:45 -05:00
0a8abaf7d5 Refactoring if condition to ensure CanCreateUser permission
Fixing UsersControllerTests
2020-03-18 18:40:21 -05:00
47c1164003 Adding logging to detect failure of CanManageWallet test
If all logs are present it's possibly issue with TempData
2020-03-18 18:17:21 -05:00
65d26ad8a1 improve redoc documentation 2020-03-18 23:35:17 +09:00
0a0d8d53a4 Improve redoc 2020-03-18 23:30:27 +09:00
e50e3f662d Can create user without authentication if there is no admin 2020-03-18 23:10:15 +09:00
540a31207e Properly validate create user input 2020-03-18 20:51:50 +09:00
132c36df7b Remove useless routes and info in swagger 2020-03-18 20:25:54 +09:00
e351e0c9ea Remove dependency on NSwag 2020-03-18 20:08:09 +09:00
8d7b9fcef2 Merge pull request from Kukks/api/user-create
Greenfield API: Create User
2020-03-18 16:26:10 +09:00
6e1f3989e8 remove special case 2020-03-18 08:10:35 +01:00
e99767c7e2 Greenfield API: Create User
Slightly big PR because I started refactoring to reduce code duplication between the UI based business logic and the api one.
2020-03-17 08:21:27 +01:00
c85fb3e89f Remove ndax from test suite (crashing exchange) 2020-03-17 13:19:55 +09:00
348934488d Refactor tests for greenfield 2020-03-16 16:36:55 +09:00
6c8918a308 Merge pull request from Kukks/api/god-mode
Greenfield API: God Mode
2020-03-16 16:29:19 +09:00
ff2ea5815c add else tests 2020-03-16 08:13:44 +01:00
cc0202ecb3 fix test 2020-03-13 08:00:04 +01:00
0c065df4bd Greenfield API: God Mode
When the `ServerManagement` permission is granted, you should be able to do everything in the system.
Maybe I should rename it to GodMode as a permission to not have any confusion with managing server settings (currently `ServerManagement`)?
2020-03-12 18:43:57 +01:00
b5664dac81 Merge pull request from Kukks/api/users-get-current
Greenfield API: Get current User
2020-03-12 23:20:18 +09:00
8173296c96 Greenfield API: Get current User
Builds on 
This PR adds a new endpoint: Get current user.. It only returns the current user's id and email for now( let's extend later)
It also adds a new permission: `ProfileManagement` which is needed for this endpoint (and for update endpoints later)
2020-03-12 14:59:24 +01:00
71a00c0e67 Merge pull request from Kukks/api/authorize-ui
Make api docs only available after login
2020-03-12 12:00:00 +09:00
70b172addc Make api docs only available after login 2020-03-11 18:05:40 +01:00
2002c6750b target netstandard2.1 for Client 2020-03-11 16:54:53 +01:00
786be9d1f5 fix tests 2020-03-11 16:54:53 +01:00
233fa8a4a1 BTCPayServer.Client library + Revoke API Key 2020-03-11 16:54:53 +01:00
c74f52a61c Merge pull request from Kukks/fix-tests
fix e2e tests
2020-03-12 00:53:44 +09:00
245507f821 fix e2e tests 2020-03-11 16:52:29 +01:00
5495c4b5d3 Merge pull request from NicolasDorier/betterrbf
Simplify RBF handling, and handle case of double spend happening outs…
2020-03-11 22:41:46 +09:00
afd2c8e3d7 Bump nbx 2020-03-11 22:32:53 +09:00
c8e1db2102 Better event messages 2020-03-11 21:11:07 +09:00
95f859b6db Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix ) 2020-03-11 21:05:12 +09:00
6bf7ef0798 Revert "Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix )"
This reverts commit 42152050a3786bd3db103220e1a45b60c9fcddaf.
2020-03-11 20:57:19 +09:00
42152050a3 Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix ) 2020-03-11 20:46:37 +09:00
67befcc629 bump 2020-03-10 22:48:17 +09:00
3cdf881438 bump lightning libraries 2020-03-10 22:31:05 +09:00
153992a458 Use good rng for generating API keys 2020-03-10 21:30:46 +09:00
691a8d6fd8 Fix warnings 2020-03-10 21:28:00 +09:00
a9bf843be0 Bump various libraries 2020-03-10 21:24:22 +09:00
60e5afe690 Merge pull request from Kukks/fix-cf
fix duplicate key error in crowdfunding
2020-03-10 20:58:09 +09:00
980bedf301 Merge pull request from pavlenex/supporter-btse
Add new supporter to readme and front page
2020-03-10 20:18:40 +09:00
6f6e8ba1a1 fix duplicate key error in crowdfunding 2020-03-10 11:20:05 +01:00
d3af82e38b Add btse to readme and front page 2020-03-10 10:28:15 +01:00
65afc9f7b2 Make sure dashboard is initialized from the beginning 2020-03-10 17:42:53 +09:00
2e630ac5d8 WaitSiteIsOperational should only wait full sync 2020-03-10 17:24:38 +09:00
e6acc19bcc Add a test catching expirationTime bug () 2020-03-10 17:11:15 +09:00
c598a1827f Remove misleading error message (Fix ) 2020-03-10 16:33:50 +09:00
223 changed files with 20595 additions and 2611 deletions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Controllers
Data
Events
Extensions.cs
Extensions
HostedServices
Hosting
JsonConverters
Models
Payments
Properties
Security
Services
Views
ZoneLimits.csbundleconfig.json
wwwroot
Build
Changelog.mdREADME.mdamd64.Dockerfilearm32v7.Dockerfilearm64v8.Dockerfilebtcpayserver.slnnuget.config

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.29" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
</Project>

@ -0,0 +1,39 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<ApiKeyData> GetCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current"), token);
return await HandleResponse<ApiKeyData>(response);
}
public virtual async Task<ApiKeyData> CreateAPIKey(CreateApiKeyRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<ApiKeyData>(response);
}
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
HandleResponse(response);
}
public virtual async Task RevokeAPIKey(string apikey, CancellationToken token = default)
{
if (apikey == null)
throw new ArgumentNullException(nameof(apikey));
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token);
HandleResponse(response);
}
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public static Uri GenerateAuthorizeUri(Uri btcpayHost, string[] permissions, bool strict = true,
bool selectiveStores = false)
{
var result = new UriBuilder(btcpayHost);
result.Path = "api-keys/authorize";
AppendPayloadToQuery(result,
new Dictionary<string, object>()
{
{"strict", strict}, {"selectiveStores", selectiveStores}, {"permissions", permissions}
});
return result.Uri;
}
}
}

@ -0,0 +1,23 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<ApplicationUserData> GetCurrentUser(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users/me"), token);
return await HandleResponse<ApplicationUserData>(response);
}
public virtual async Task<ApplicationUserData> CreateUser(CreateApplicationUserRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
return await HandleResponse<ApplicationUserData>(response);
}
}
}

@ -0,0 +1,117 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
private readonly string _apiKey;
private readonly Uri _btcpayHost;
private readonly string _username;
private readonly string _password;
private readonly HttpClient _httpClient;
public string APIKey => _apiKey;
public BTCPayServerClient(Uri btcpayHost, HttpClient httpClient = null)
{
if (btcpayHost == null)
throw new ArgumentNullException(nameof(btcpayHost));
_btcpayHost = btcpayHost;
_httpClient = httpClient ?? new HttpClient();
}
public BTCPayServerClient(Uri btcpayHost, string APIKey, HttpClient httpClient = null)
{
_apiKey = APIKey;
_btcpayHost = btcpayHost;
_httpClient = httpClient ?? new HttpClient();
}
public BTCPayServerClient(Uri btcpayHost, string username, string password, HttpClient httpClient = null)
{
_apiKey = APIKey;
_btcpayHost = btcpayHost;
_username = username;
_password = password;
_httpClient = httpClient ?? new HttpClient();
}
protected void HandleResponse(HttpResponseMessage message)
{
message.EnsureSuccessStatusCode();
}
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
{
HandleResponse(message);
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
}
protected virtual HttpRequestMessage CreateHttpRequest(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null)
{
UriBuilder uriBuilder = new UriBuilder(_btcpayHost) {Path = path};
if (queryPayload != null && queryPayload.Any())
{
AppendPayloadToQuery(uriBuilder, queryPayload);
}
var httpRequest = new HttpRequestMessage(method ?? HttpMethod.Get, uriBuilder.Uri);
if (_apiKey != null)
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey);
else if (!string.IsNullOrEmpty(_username))
{
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", System.Convert.ToBase64String(Encoding.ASCII.GetBytes(_username + ":" + _password)));
}
return httpRequest;
}
protected virtual HttpRequestMessage CreateHttpRequest<T>(string path,
Dictionary<string, object> queryPayload = null,
T bodyPayload = default, HttpMethod method = null)
{
var request = CreateHttpRequest(path, queryPayload, method);
if (typeof(T).IsPrimitive || !EqualityComparer<T>.Default.Equals(bodyPayload, default(T)))
{
request.Content = new StringContent(JsonConvert.SerializeObject(bodyPayload), Encoding.UTF8, "application/json");
}
return request;
}
private static void AppendPayloadToQuery(UriBuilder uri, Dictionary<string, object> payload)
{
if (uri.Query.Length > 1)
uri.Query += "&";
foreach (KeyValuePair<string, object> keyValuePair in payload)
{
UriBuilder uriBuilder = uri;
if (keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
{
foreach (var item in (IEnumerable)keyValuePair.Value)
{
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
Uri.EscapeDataString(item.ToString()) + "&";
}
}
else
{
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
Uri.EscapeDataString(keyValuePair.Value.ToString()) + "&";
}
}
uri.Query = uri.Query.Trim('&');
}
}
}

@ -0,0 +1,34 @@
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.Client.JsonConverters
{
public class PermissionJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Permission).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException("Type 'Permission' is expected to be a 'String'", reader);
if (reader.Value is String s && Permission.TryParse(s, out var permission))
return permission;
throw new JsonObjectException("Invalid 'Permission' String", reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is Permission v)
writer.WriteValue(v.ToString());
}
}
}

@ -0,0 +1,13 @@
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class ApiKeyData
{
public string ApiKey { get; set; }
public string Label { get; set; }
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
public Permission[] Permissions { get; set; }
}
}

@ -0,0 +1,38 @@
namespace BTCPayServer.Client.Models
{
public class ApplicationUserData
{
/// <summary>
/// the id of the user
/// </summary>
public string Id { get; set; }
/// <summary>
/// the email AND username of the user
/// </summary>
public string Email { get; set; }
/// <summary>
/// Whether the user has verified their email
/// </summary>
public bool EmailConfirmed { get; set; }
/// <summary>
/// whether the user needed to verify their email on account creation
/// </summary>
public bool RequiresEmailConfirmation { get; set; }
}
public class CreateApplicationUserRequest
{
/// <summary>
/// the email AND username of the new user
/// </summary>
public string Email { get; set; }
/// <summary>
/// password of the new user
/// </summary>
public string Password { get; set; }
/// <summary>
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
/// </summary>
public bool? IsAdministrator { get; set; }
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class CreateApiKeyRequest
{
public string Label { get; set; }
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
public Permission[] Permissions { get; set; }
}
}

@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BTCPayServer.Client
{
public class Policies
{
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
public const string CanViewProfile = "btcpay.user.canviewprofile";
public const string CanCreateUser = "btcpay.server.cancreateuser";
public const string Unrestricted = "unrestricted";
public static IEnumerable<string> AllPolicies
{
get
{
yield return CanCreateInvoice;
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
yield return CanViewStoreSettings;
yield return CanModifyProfile;
yield return CanViewProfile;
yield return CanCreateUser;
yield return Unrestricted;
}
}
public static bool IsValidPolicy(string policy)
{
return AllPolicies.Any(p => p.Equals(policy, StringComparison.OrdinalIgnoreCase));
}
public static bool IsStorePolicy(string policy)
{
return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase);
}
public static bool IsServerPolicy(string policy)
{
return policy.StartsWith("btcpay.server", StringComparison.OrdinalIgnoreCase);
}
}
public class Permission
{
public static Permission Create(string policy, string storeId = null)
{
if (TryCreatePermission(policy, storeId, out var r))
return r;
throw new ArgumentException("Invalid Permission");
}
public static bool TryCreatePermission(string policy, string storeId, out Permission permission)
{
permission = null;
if (policy == null)
throw new ArgumentNullException(nameof(policy));
policy = policy.Trim().ToLowerInvariant();
if (!Policies.IsValidPolicy(policy))
return false;
if (storeId != null && !Policies.IsStorePolicy(policy))
return false;
permission = new Permission(policy, storeId);
return true;
}
public static bool TryParse(string str, out Permission permission)
{
permission = null;
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
var separator = str.IndexOf(':');
if (separator == -1)
{
str = str.ToLowerInvariant();
if (!Policies.IsValidPolicy(str))
return false;
permission = new Permission(str, null);
return true;
}
else
{
var policy = str.Substring(0, separator).ToLowerInvariant();
if (!Policies.IsValidPolicy(policy))
return false;
if (!Policies.IsStorePolicy(policy))
return false;
var storeId = str.Substring(separator + 1);
if (storeId.Length == 0)
return false;
permission = new Permission(policy, storeId);
return true;
}
}
internal Permission(string policy, string storeId)
{
Policy = policy;
StoreId = storeId;
}
public bool Contains(Permission subpermission)
{
if (subpermission is null)
throw new ArgumentNullException(nameof(subpermission));
if (!ContainsPolicy(subpermission.Policy))
{
return false;
}
if (!Policies.IsStorePolicy(subpermission.Policy))
return true;
return StoreId == null || subpermission.StoreId == this.StoreId;
}
public static IEnumerable<Permission> ToPermissions(string[] permissions)
{
if (permissions == null)
throw new ArgumentNullException(nameof(permissions));
foreach (var p in permissions)
{
if (TryParse(p, out var pp))
yield return pp;
}
}
private bool ContainsPolicy(string subpolicy)
{
if (this.Policy == Policies.Unrestricted)
return true;
if (this.Policy == subpolicy)
return true;
if (subpolicy == Policies.CanViewStoreSettings && this.Policy == Policies.CanModifyStoreSettings)
return true;
if (subpolicy == Policies.CanCreateInvoice && this.Policy == Policies.CanModifyStoreSettings)
return true;
if (subpolicy == Policies.CanViewProfile && this.Policy == Policies.CanModifyProfile)
return true;
return false;
}
public string StoreId { get; }
public string Policy { get; }
public override string ToString()
{
if (StoreId != null)
{
return $"{Policy}:{StoreId}";
}
return Policy;
}
public override bool Equals(object obj)
{
Permission item = obj as Permission;
if (item == null)
return false;
return ToString().Equals(item.ToString());
}
public static bool operator ==(Permission a, Permission b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToString() == b.ToString();
}
public static bool operator !=(Permission a, Permission b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
}
}

@ -24,6 +24,7 @@ namespace BTCPayServer
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()

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

@ -51,6 +51,28 @@ namespace BTCPayServer
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
});
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "LCAD",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"LCAD_CAD = 1",
"LCAD_X = CAD_BTC * BTC_X",
"LCAD_BTC = bylls(CAD_BTC)",
},
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
});
}
}

@ -25,7 +25,11 @@ namespace BTCPayServer
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}";
//precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000
//precision 8: 10 = 10
var money = new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
return $"{base.GenerateBIP21(cryptoInfoAddress, money)}&assetid={AssetId}";
}
}
}

@ -14,7 +14,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("fee")] public long Fee { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }

@ -18,7 +18,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("fee")] public long Fee { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }

@ -61,6 +61,8 @@ namespace BTCPayServer
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string UriScheme { get; internal set; }
public bool SupportPayJoin { get; set; } = false;
public KeyPath GetRootKeyPath(DerivationType type)
{
KeyPath baseKey;

@ -4,6 +4,6 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.2" />
<PackageReference Include="NBXplorer.Client" Version="3.0.9" />
</ItemGroup>
</Project>

@ -5,8 +5,8 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.1.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
</ItemGroup>
</Project>

@ -22,18 +22,17 @@ namespace BTCPayServer.Data
[MaxLength(50)] public string UserId { get; set; }
public APIKeyType Type { get; set; } = APIKeyType.Legacy;
public string Permissions { get; set; }
public byte[] Blob { get; set; }
public StoreData StoreData { get; set; }
public ApplicationUser User { get; set; }
public string Label { get; set; }
public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; }
}
public void SetPermissions(IEnumerable<string> permissions)
{
Permissions = string.Join(';',
permissions?.Select(s => s.Replace(";", string.Empty)) ?? new string[0]);
}
public class APIKeyBlob
{
public string[] Permissions { get; set; }
}
public enum APIKeyType

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

@ -35,6 +35,9 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<PlannedTransaction> PlannedTransactions { get; set; }
public DbSet<PayjoinLock> PayjoinLocks { get; set; }
public DbSet<AppData> Apps
{
get; set;
@ -45,6 +48,8 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<OffchainTransactionData> OffchainTransactions { get; set; }
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Data
{
public class OffchainTransactionData
{
[Key]
[MaxLength(32*2)]
public string Id { get; set; }
public byte[] Blob { get; set; }
}
}

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Data
{
/// <summary>
/// We represent the locks of the PayjoinRepository
/// with this table. (Both, our utxo we locked as part of a payjoin
/// and the utxo of the payer which were used to pay us)
/// </summary>
public class PayjoinLock
{
[Key]
[MaxLength(100)]
public string Id { get; set; }
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

@ -0,0 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Data
{
public class PlannedTransaction
{
[Key]
[MaxLength(100)]
// Id in the format [cryptocode]-[txid]
public string Id { get; set; }
public DateTimeOffset BroadcastAt { get; set; }
public byte[] Blob { get; set; }
}
}

@ -0,0 +1,42 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20200402065615_AddApiKeyBlob")]
public partial class AddApiKeyBlob : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Permissions",
table: "ApiKeys");
}
migrationBuilder.AddColumn<byte[]>(
name: "Blob",
table: "ApiKeys",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Blob",
table: "ApiKeys");
}
migrationBuilder.AddColumn<string>(
name: "Permissions",
table: "ApiKeys",
type: "TEXT",
nullable: true);
}
}
}

@ -0,0 +1,59 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20200413052418_PlannedTransactions")]
public partial class PlannedTransactions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlannedTransactions",
columns: table => new
{
Id = table.Column<string>(maxLength: 100, nullable: false),
BroadcastAt = table.Column<DateTimeOffset>(nullable: false),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlannedTransactions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PayjoinLocks",
columns: table => new
{
Id = table.Column<string>(maxLength: 100, nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_PayjoinLocks", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OffchainTransactions",
columns: table => new
{
Id = table.Column<string>(maxLength: 64, nullable: false),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OffchainTransactions", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PayjoinLocks");
migrationBuilder.DropTable(
name: "PlannedTransactions");
migrationBuilder.DropTable(
name: "OffchainTransactions");
}
}
}

@ -22,10 +22,10 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("Label")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Permissions")
b.Property<string>("Label")
.HasColumnType("TEXT");
b.Property<string>("StoreId")
@ -240,6 +240,20 @@ namespace BTCPayServer.Migrations
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.OffchainTransactionData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(64);
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.HasKey("Id");
b.ToTable("OffchainTransactions");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
@ -297,6 +311,17 @@ namespace BTCPayServer.Migrations
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PayjoinLock", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(100);
b.HasKey("Id");
b.ToTable("PayjoinLocks");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
@ -356,6 +381,23 @@ namespace BTCPayServer.Migrations
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.PlannedTransaction", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(100);
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<DateTimeOffset>("BroadcastAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("PlannedTransactions");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")

@ -99,7 +99,7 @@ namespace BTCPayServer.Services.Rates
{
LastRequested = LastRequested
};
if (_Latest is LatestFetch fetch)
if (_Latest is LatestFetch fetch && fetch.Latest is PairRate[])
{
state.LastUpdated = fetch.Updated;
state.Rates = fetch.Latest

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitflyerRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public BitflyerRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.bitflyer.jp/v1/ticker", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
if (jobj.Property("error_message")?.Value?.Value<string>() is string err)
{
throw new Exception($"Error from bitflyer: {err}");
}
var bid = jobj.Property("best_bid").Value.Value<decimal>();
var ask = jobj.Property("best_ask").Value.Value<decimal>();
var rates = new PairRate[1];
rates[0] = new PairRate(CurrencyPair.Parse(jobj.Property("product_code").Value.Value<string>()), new BidAsk(bid, ask));
return rates;
}
}
}

@ -77,6 +77,7 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
yield return new AvailableRateProvider("bitflyer", "Bitflyer", "https://api.bitflyer.com/v1/ticker");
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis");
@ -100,6 +101,7 @@ namespace BTCPayServer.Services.Rates
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));

@ -4,11 +4,12 @@ using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security.APIKeys;
using BTCPayServer.Security.GreenField;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using ExchangeSharp;
using Newtonsoft.Json;
using OpenQA.Selenium;
using Xunit;
@ -23,7 +24,7 @@ namespace BTCPayServer.Tests
public const string TestApiPath = "api/test/apikey";
public ApiKeysTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
@ -42,61 +43,59 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
await user.CreateStoreAsync();
await user.MakeAdmin(false);
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
if (!user.IsAdmin)
{
//not an admin, so this permission should not show
Assert.DoesNotContain("ServerManagementPermission", s.Driver.PageSource);
await user.MakeAdmin();
s.Logout();
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
}
//not an admin, so this permission should not show
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
await user.MakeAdmin();
s.Logout();
s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
//server management should show now
s.SetCheckbox(s, "ServerManagementPermission", true);
s.SetCheckbox(s, "StoreManagementPermission", true);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.SetCheckbox(s, "btcpay.user.canviewprofile", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
//this api key has access to everything
await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement,
APIKeyConstants.Permissions.StoreManagement);
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings,Policies.CanModifyStoreSettings, Policies.CanViewProfile);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.SetCheckbox(s, "ServerManagementPermission", true);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
APIKeyConstants.Permissions.ServerManagement);
Policies.CanModifyServerSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.SetCheckbox(s, "StoreManagementPermission", true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
APIKeyConstants.Permissions.StoreManagement);
Policies.CanModifyStoreSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click();
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
//there should be a store already by default in the dropdown
var dropdown = s.Driver.FindElement(By.Name("SpecificStores[0]"));
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[2].SpecificStores[0]"));
var option = dropdown.FindElement(By.TagName("option"));
var storeId = option.GetAttribute("value");
option.Click();
s.Driver.FindElement(By.Id("Generate")).Click();
var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
APIKeyConstants.Permissions.GetStorePermission(storeId));
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.Id("Generate")).Click();
@ -117,37 +116,14 @@ namespace BTCPayServer.Tests
//permissions
//strict
//selectiveStores
UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri);
authorize.Path = "api-keys/authorize";
authorize.AppendPayloadToQuery(new Dictionary<string, object>()
{
{"redirect", "https://local.local/callback"},
{"applicationName", "kukksappname"},
{"strict", true},
{"selectiveStores", false},
{
"permissions",
new[]
{
APIKeyConstants.Permissions.StoreManagement,
APIKeyConstants.Permissions.ServerManagement
}
},
});
var authUrl = authorize.ToString();
var perms = new[]
{
APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement
};
authUrl = authUrl.Replace("permissions=System.String%5B%5D",
string.Join("&", perms.Select(s1 => $"permissions={s1}")));
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
s.Driver.PageSource.Contains("kukksappname");
Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected);
Assert.NotNull(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly"));
Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected);
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
var url = s.Driver.Url;
@ -155,109 +131,127 @@ namespace BTCPayServer.Tests
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
authorize = new UriBuilder(tester.PayTester.ServerUri);
authorize.Path = "api-keys/authorize";
authorize.AppendPayloadToQuery(new Dictionary<string, object>()
{
{"strict", false},
{"selectiveStores", true},
{
"permissions",
new[]
{
APIKeyConstants.Permissions.StoreManagement,
APIKeyConstants.Permissions.ServerManagement
}
}
});
authUrl = authorize.ToString();
perms = new[]
{
APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement
};
authUrl = authUrl.Replace("permissions=System.String%5B%5D",
string.Join("&", perms.Select(s1 => $"permissions={s1}")));
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetBlob().Permissions);
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
Assert.Null(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected);
Assert.Null(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly"));
Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected);
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
s.SetCheckbox(s, "ServerManagementPermission", false);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false);
Assert.Contains("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
url = s.Driver.Url;
results = url.Split("?").Last().Split("&")
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetBlob().Permissions);
}
}
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount,
params string[] permissions)
params string[] expectedPermissionsArr)
{
var resultUser =
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id",
tester.PayTester.HttpClient);
Assert.Equal(testAccount.UserId, resultUser);
var expectedPermissions = Permission.ToPermissions(expectedPermissionsArr).ToArray();
expectedPermissions ??= new Permission[0];
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
var permissions = apikeydata.Permissions;
Assert.Equal(expectedPermissions.Length, permissions.Length);
foreach (var expectPermission in expectedPermissions)
{
Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}");
}
if (permissions.Contains(Permission.Create(Policies.CanViewProfile)))
{
var resultUser = await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
Assert.Equal(testAccount.UserId, resultUser);
}
else
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
});
}
//create a second user to see if any of its data gets messed upin our results.
var secondUser = tester.NewAccount();
secondUser.GrantAccess();
var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions);
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any())
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
var unrestricted = Permission.Create(Policies.Unrestricted, null);
var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Policies.CanModifyStoreSettings);
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
{
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
foreach (string selectiveStorePermission in selectiveStorePermissions)
foreach (var selectiveStorePermission in selectiveStorePermissions)
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{selectiveStorePermission}/can-edit",
$"{TestApiPath}/me/stores/{selectiveStorePermission.StoreId}/can-edit",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase));
data => data.Id.Equals(selectiveStorePermission.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement))
bool shouldBeAuthorized = false;
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Policies.CanViewStoreSettings, testAccount.StoreId)))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/actions",
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
shouldBeAuthorized = true;
}
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Policies.CanModifyStoreSettings, testAccount.StoreId)))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
tester.PayTester.HttpClient));
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
shouldBeAuthorized = true;
}
else
if (!shouldBeAuthorized)
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/actions",
tester.PayTester.HttpClient);
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
tester.PayTester.HttpClient);
});
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
else
else if (!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
@ -265,19 +259,42 @@ namespace BTCPayServer.Tests
tester.PayTester.HttpClient);
});
}
else
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient);
}
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
if (!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
else
{
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement))
if (permissions.Contains(canModifyServer))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin",
tester.PayTester.HttpClient));
}
else
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin",
tester.PayTester.HttpClient);
});
}
}
public async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)

@ -23,9 +23,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="79.0.3945.3600" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="80.0.3987.10600" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

@ -1,4 +1,6 @@
using BTCPayServer.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
@ -16,7 +18,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
@ -92,10 +93,12 @@ namespace BTCPayServer.Tests
}
public bool MockRates { get; set; } = true;
public string SocksEndpoint { get; set; }
public HashSet<string> Chains { get; set; } = new HashSet<string>(){"BTC"};
public bool UseLightning { get; set; }
public bool AllowAdminRegistration { get; set; } = true;
public bool DisableRegistration { get; set; } = false;
public async Task StartAsync()
{
if (!Directory.Exists(_Directory))
@ -137,9 +140,11 @@ namespace BTCPayServer.Tests
config.AppendLine($"lbtc.explorer.url={LBTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"lbtc.explorer.cookiefile=0");
}
config.AppendLine("allow-admin-registration=1");
if (AllowAdminRegistration)
config.AppendLine("allow-admin-registration=1");
config.AppendLine($"torrcfile={TestUtils.GetTestDataFullPath("Tor/torrc")}");
config.AppendLine($"socksendpoint={SocksEndpoint}");
config.AppendLine($"debuglog=debug.log");
@ -161,7 +166,7 @@ namespace BTCPayServer.Tests
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", DisableRegistration ? "true" : "false" });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.UseContentRoot(FindBTCPayServerDirectory())
@ -177,6 +182,10 @@ namespace BTCPayServer.Tests
.AddProvider(Logs.LogProvider);
});
})
.ConfigureServices(services =>
{
services.TryAddSingleton<IFeeProviderFactory>(new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1)));
})
.UseKestrel()
.UseStartup<Startup>()
.Build();
@ -221,6 +230,10 @@ namespace BTCPayServer.Tests
var bitfinex = new MockRateProvider();
bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m)));
rateProvider.Providers.Add("bitfinex", bitfinex);
var bitpay = new MockRateProvider();
bitpay.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETB_BTC"), new BidAsk(0.1m)));
rateProvider.Providers.Add("bitpay", bitpay);
}
@ -231,23 +244,13 @@ namespace BTCPayServer.Tests
private async Task WaitSiteIsOperational()
{
_ = HttpClient.GetAsync("/").ConfigureAwait(false);
using (var cts = new CancellationTokenSource(20_000))
{
var synching = WaitIsFullySynched(cts.Token);
var accessingHomepage = WaitCanAccessHomepage(cts.Token);
await Task.WhenAll(synching, accessingHomepage).ConfigureAwait(false);
}
}
private async Task WaitCanAccessHomepage(CancellationToken cancellationToken)
{
while (true)
{
var resp = await HttpClient.GetAsync("/", cancellationToken).ConfigureAwait(false);
if (resp.StatusCode == HttpStatusCode.OK)
break;
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
await Task.WhenAll(synching).ConfigureAwait(false);
}
// Opportunistic call to wake up view compilation in debug mode, we don't need to await.
}
private async Task WaitIsFullySynched(CancellationToken cancellationToken)

@ -205,7 +205,8 @@ namespace BTCPayServer.Tests
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
var iframe = s.Driver.SwitchTo().Frame("btcpay");
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
var iframe = s.Driver.SwitchTo().Frame(frameElement);
closebutton = iframe.FindElement(By.ClassName("close-action"));
Assert.True(closebutton.Displayed);
});

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using NBitcoin;
using NBitcoin.Payment;
using NBitcoin.RPC;
using NBitpayClient;
using Xunit;
@ -79,20 +80,28 @@ namespace BTCPayServer.Tests
user.GrantAccess();
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("USDT");
user.RegisterDerivationScheme("ETB");
await tester.LBTCExplorerNode.GenerateAsync(4);
//no tether on our regtest, lets create it and set it
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
var lbtc = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
var etb = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("ETB");
var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network)
.AssetId = tether.AssetId;
Logs.Tester.LogInformation($"Asset is {tether.AssetId}");
Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT").AssetId);
Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId);
var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("ETB").Network)
.AssetId = etb.AssetId;
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
var ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("LBTC"));
//1 lbtc = 1 btc
Assert.Equal(1, ci.Rate);
@ -109,7 +118,7 @@ namespace BTCPayServer.Tests
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
1, "UNSET", tether.AssetId);
@ -120,6 +129,14 @@ namespace BTCPayServer.Tests
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments);
});
//test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21.Replace(etb.UriScheme, "bitcoin"), etb.NBitcoinNetwork);
//precision = 2, 1ETB = 0.00000100
Assert.Equal( 100,etbBip21.Amount.Satoshi);
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21.Replace(lbtc.UriScheme, "bitcoin"), lbtc.NBitcoinNetwork);
//precision = 8, 0.1 = 0.1
Assert.Equal( 0.1m,lbtcBip21.Amount.ToDecimal(MoneyUnit.BTC));
}
}
}

@ -88,6 +88,10 @@ namespace BTCPayServer.Tests
if (!webElement.Displayed)
return;
}
catch (NoSuchWindowException)
{
return;
}
catch (NoSuchElementException)
{
return;

@ -1,16 +1,17 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Controllers.RestApi.ApiKeys;
using BTCPayServer.Data;
using BTCPayServer.Security.APIKeys;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNet.SignalR.Client;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
namespace BTCPayServer.Tests
{
@ -22,11 +23,11 @@ namespace BTCPayServer.Tests
public GreenfieldAPITests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ApiKeysControllerTests()
{
@ -36,36 +37,196 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
string apiKey = await GenerateAPIKey(tester, user);
var client = await user.CreateClient(Policies.CanViewProfile);
var clientBasic = await user.CreateClient();
//Get current api key
var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/api-keys/current");
request.Headers.Authorization = new AuthenticationHeaderValue("token", apiKey);
var result = await tester.PayTester.HttpClient.SendAsync(request);
Assert.True(result.IsSuccessStatusCode);
var apiKeyData = JObject.Parse(await result.Content.ReadAsStringAsync()).ToObject<ApiKeyData>();
var apiKeyData = await client.GetCurrentAPIKeyInfo();
Assert.NotNull(apiKeyData);
Assert.Equal(apiKey, apiKeyData.ApiKey);
Assert.Equal(user.UserId, apiKeyData.UserId);
Assert.Equal(2, apiKeyData.Permissions.Length);
Assert.Equal(client.APIKey, apiKeyData.ApiKey);
Assert.Single(apiKeyData.Permissions);
//a client using Basic Auth has no business here
await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo());
//revoke current api key
await client.RevokeCurrentAPIKeyInfo();
await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo());
//a client using Basic Auth has no business here
await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo());
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteAPIKeyViaAPI()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var acc = tester.NewAccount();
await acc.GrantAccessAsync();
var unrestricted = await acc.CreateClient();
var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
});
Assert.Equal("Hello world", apiKey.Label);
var p = Assert.Single(apiKey.Permissions);
Assert.Equal(Policies.CanViewProfile, p.Policy);
var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey);
await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world2",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
}));
await unrestricted.RevokeAPIKey(apiKey.ApiKey);
await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey));
}
}
private static async Task<string> GenerateAPIKey(ServerTester tester, TestAccount user)
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateUsersViaAPI()
{
var manageController = tester.PayTester.GetController<ManageController>(user.UserId, user.StoreId, user.IsAdmin);
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
new ManageController.AddApiKeyViewModel()
using (var tester = ServerTester.Create(newDb: true))
{
tester.PayTester.DisableRegistration = true;
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com" }));
// Pass too simple
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" }));
// We have no admin, so it should work
var user1 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
// We have no admin, so it should work
var user2 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
// Duplicate email
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }));
// Let's make an admin
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true });
// Creating a new user without proper creds is now impossible (unauthorized)
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
await AssertHttpError(401, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }));
// But should be ok with subscriptions unlocked
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" });
// But it should be forbidden to create an admin without being authenticated
await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = true });
var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id;
adminAcc.IsAdmin = true;
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
// We should be forbidden to create a new user without proper admin permissions
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
// However, should be ok with the unrestricted permissions of an admin
adminClient = await adminAcc.CreateClient(Policies.Unrestricted);
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" });
// Even creating new admin should be ok
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true });
var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false;
var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings);
// User1 trying to get server management would still fail to create user
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }));
// User1 should be able to create user if subscription unlocked
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" });
// But not an admin
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
}
}
private async Task AssertHttpError(int code, Func<Task> act)
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
Assert.Contains(code.ToString(), ex.Message);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task UsersControllerTests()
{
using (var tester = ServerTester.Create(newDb: true))
{
tester.PayTester.DisableRegistration = true;
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var clientProfile = await user.CreateClient(Policies.CanModifyProfile);
var clientServer = await user.CreateClient(Policies.CanCreateUser, Policies.CanViewProfile);
var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings);
var clientBasic = await user.CreateClient();
var apiKeyProfileUserData = await clientProfile.GetCurrentUser();
Assert.NotNull(apiKeyProfileUserData);
Assert.Equal(apiKeyProfileUserData.Id, user.UserId);
Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email);
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
await clientServer.GetCurrentUser();
await clientProfile.GetCurrentUser();
await clientBasic.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{
ServerManagementPermission = true,
StoreManagementPermission = true,
StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
}));
var statusMessage = manageController.TempData.GetStatusMessageModel();
Assert.NotNull(statusMessage);
var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf("<code>") + 6);
apiKey = apiKey.Substring(0, apiKey.IndexOf("</code>") );
return apiKey;
var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
});
Assert.NotNull(newUser);
var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
});
Assert.NotNull(newUser2);
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}",
Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
}));
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Password = Guid.NewGuid().ToString()
}));
}
}
}
}

@ -0,0 +1,771 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.Payment;
using NBitpayClient;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class PayJoinTests
{
public const int TestTimeout = 60_000;
public PayJoinTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseTheDelayedBroadcaster()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
await broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromDays(500), RandomTransaction(network), network);
var tx = RandomTransaction(network);
await broadcaster.Schedule(DateTimeOffset.UtcNow - TimeSpan.FromDays(5), tx, network);
// twice on same tx should be noop
await broadcaster.Schedule(DateTimeOffset.UtcNow - TimeSpan.FromDays(5), tx, network);
broadcaster.Disable();
Assert.Equal(0, await broadcaster.ProcessAll());
broadcaster.Enable();
Assert.Equal(1, await broadcaster.ProcessAll());
Assert.Equal(0, await broadcaster.ProcessAll());
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUsePayjoinRepository()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var repo = tester.PayTester.GetService<PayJoinRepository>();
var outpoint = RandomOutpoint();
// Should not be locked
Assert.False(await repo.TryUnlock(outpoint));
// Can lock input
Assert.True(await repo.TryLockInputs(new [] { outpoint }));
// Can't twice
Assert.False(await repo.TryLockInputs(new [] { outpoint }));
Assert.False(await repo.TryUnlock(outpoint));
// Lock and unlock outpoint utxo
Assert.True(await repo.TryLock(outpoint));
Assert.True(await repo.TryUnlock(outpoint));
Assert.False(await repo.TryUnlock(outpoint));
}
}
private Transaction RandomTransaction(BTCPayNetwork network)
{
var tx = network.NBitcoinNetwork.CreateTransaction();
tx.Inputs.Add(new OutPoint(RandomUtils.GetUInt256(), 0), Script.Empty);
tx.Outputs.Add(Money.Coins(1.0m), new Key().ScriptPubKey);
return tx;
}
private OutPoint RandomOutpoint()
{
return new OutPoint(RandomUtils.GetUInt256(), 0);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanOnlyUseCorrectAddressFormatsForPayjoin()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
.AssertType<ScriptPubKeyType[]>()
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
{
var senderUser = tester.NewAccount();
senderUser.GrantAccess(true);
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
{
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
var receiverUser = tester.NewAccount();
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true);
await receiverUser.EnablePayJoin();
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
var clientShouldError = unsupportedFormats.Contains(senderAddressType);
var errorCode = ( unsupportedFormats.Contains( receiverAddressType) || receiverAddressType != senderAddressType)? "unsupported-inputs" : null;
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = 50000, Currency = "sats", FullNotifications = true});
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.AddCoins(senderCoin);
txBuilder.Send(invoiceAddress, invoice.BtcDue);
txBuilder.SetChange(await senderUser.GetNewAddress(network));
txBuilder.SendEstimatedFees(new FeeRate(50m));
var psbt = txBuilder.BuildPSBT(false);
psbt = await senderUser.Sign(psbt);
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, clientShouldError);
}
}
}
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUsePayjoinViaUI()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
// var payjoinRepository = s.Server.PayTester.GetService<PayJoinRepository>();
// var broadcaster = s.Server.PayTester.GetService<DelayedTransactionBroadcaster>();
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
//payjoin is not enabled by default.
var invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToHome();
s.GoToStore(receiver.storeId);
//payjoin is not enabled by default.
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
s.SetCheckbox(s,"PayJoinEnabled", true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true);
var senderWalletId = new WalletId(sender.storeId, "BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToWalletSend(senderWalletId);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value")));
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
return Task.CompletedTask;
});
//no funds in receiver wallet to do payjoin
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
});
s.GoToInvoices();
var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value"));
Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase));
//let's do it all again, except now the receiver has funds and is able to payjoin
invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
s.GoToWalletSend(senderWalletId);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("1");
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
return Task.CompletedTask;
});
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
var payments = invoice.GetPayments();
Assert.Equal(2, payments.Count);
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
Assert.Equal(-1, ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
Assert.Equal(0, ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
Assert.False(originalPayment.Accounted);
Assert.True(coinjoinPayment.Accounted);
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
Assert.Equal(originalPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value,
coinjoinPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value);
});
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
var dto = invoice.EntityToDTO();
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
});
s.GoToInvoices();
paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value"));
Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase));
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUsePayjoinFeeCornerCase()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var senderUser = tester.NewAccount();
senderUser.GrantAccess(true);
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
var receiverUser = tester.NewAccount();
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
await receiverUser.EnablePayJoin();
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string lastInvoiceId = null;
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
async Task<PSBT> RunVector(bool skipLockedCheck = false)
{
var coin = await senderUser.ReceiveUTXO(vector.SpentCoin, network);
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true});
lastInvoiceId = invoice.Id;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.AddCoins(coin);
txBuilder.Send(invoiceAddress, vector.Paid);
txBuilder.SendFees(vector.Fee);
txBuilder.SetChange(await senderUser.GetNewAddress(network));
var psbt = txBuilder.BuildPSBT(false);
psbt = await senderUser.Sign(psbt);
var pj = await senderUser.SubmitPayjoin(invoice, psbt, vector.ExpectedError);
if (vector.ExpectedError is null)
{
Assert.Contains(pj.Inputs, o => o.PrevOut == receiverCoin.Outpoint);
if (!skipLockedCheck)
Assert.True(await payjoinRepository.TryUnlock(receiverCoin.Outpoint));
}
else
{
Assert.Null(pj);
if (!skipLockedCheck)
Assert.False(await payjoinRepository.TryUnlock(receiverCoin.Outpoint));
}
if (vector.InvoicePaid)
{
await TestUtils.EventuallyAsync(async () =>
{
invoice = await receiverUser.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
}
return pj;
}
async Task LockAllButReceiverCoin()
{
var coins = await btcPayWallet.GetUnspentCoins(receiverUser.DerivationScheme);
foreach (var coin in coins)
{
if (coin.OutPoint != receiverCoin.Outpoint)
await payjoinRepository.TryLock(coin.OutPoint);
else
await payjoinRepository.TryUnlock(coin.OutPoint);
}
}
Logs.Tester.LogInformation("Here we send exactly the right amount. This should fails as\n" +
"there is not enough to pay the additional payjoin input. (going below the min relay fee" +
"However, the original tx has been broadcasted!");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
await RunVector();
await LockAllButReceiverCoin();
Logs.Tester.LogInformation("We don't pay enough");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid");
await RunVector();
Logs.Tester.LogInformation("We pay correctly");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
await RunVector();
await LockAllButReceiverCoin();
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
"The receiver should have added a fake output");
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
var proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
await LockAllButReceiverCoin();
Logs.Tester.LogInformation("We pay correctly, but no utxo\n" +
"However, this has the side effect of having the receiver broadcasting the original tx");
await payjoinRepository.TryLock(receiverCoin.Outpoint);
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "out-of-utxos");
await RunVector(true);
await LockAllButReceiverCoin();
var originalSenderUser = senderUser;
retry:
// Additional fee is 96 , minrelaytx is 294
// We pay correctly, fees partially taken from what is overpaid
// We paid 510, the receiver pay 10 sat
// The send pay remaining 86 sat from his pocket
// So total paid by sender should be 86 + 510 + 200 so we should get 1090 - (86 + 510 + 200) == 294 back)
Logs.Tester.LogInformation($"Check if we can take fee on overpaid utxo{(senderUser == receiverUser ? " (to self)" : "")}");
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
Assert.Contains(proposedPSBT.Outputs, o => o.Value == Money.Satoshis(500) + receiverCoin.Amount);
Assert.Contains(proposedPSBT.Outputs, o => o.Value == Money.Satoshis(294));
proposedPSBT = await senderUser.Sign(proposedPSBT);
proposedPSBT = proposedPSBT.Finalize();
var explorerClient = tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient(proposedPSBT.Network.NetworkSet.CryptoCode);
var result = await explorerClient.BroadcastAsync(proposedPSBT.ExtractTransaction());
Assert.True(result.Success);
Logs.Tester.LogInformation($"We broadcasted the payjoin {proposedPSBT.ExtractTransaction().GetHash()}");
Logs.Tester.LogInformation($"Let's make sure that the coinjoin is not over paying, since the 10 overpaid sats have gone to fee");
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(lastInvoiceId);
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
Assert.Equal(InvoiceExceptionStatus.None, invoice.ExceptionStatus);
var coins = await btcPayWallet.GetUnspentCoins(receiverUser.DerivationScheme);
foreach (var coin in coins)
await payjoinRepository.TryLock(coin.OutPoint);
});
tester.ExplorerNode.Generate(1);
receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
await LockAllButReceiverCoin();
if (senderUser != receiverUser)
{
Logs.Tester.LogInformation("Let's do the same, this time paying to ourselves");
senderUser = receiverUser;
goto retry;
}
else
{
senderUser = originalSenderUser;
}
// Same as above. Except the sender send one satoshi less, so the change
// output would get below dust and would be removed completely.
// So we remove as much fee as we can, and still accept the transaction because it is above minrelay fee
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
// We should have our payment
Assert.Contains(proposedPSBT.Outputs, output => output.Value == Money.Satoshis(500) + receiverCoin.Amount);
// Plus our other change output with value just at dust level
Assert.Contains(proposedPSBT.Outputs, output => output.Value == Money.Satoshis(294));
proposedPSBT = await senderUser.Sign(proposedPSBT);
proposedPSBT = proposedPSBT.Finalize();
explorerClient = tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient(proposedPSBT.Network.NetworkSet.CryptoCode);
result = await explorerClient.BroadcastAsync(proposedPSBT.ExtractTransaction(), true);
Assert.True(result.Success);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUsePayjoin()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
////var payJoinStateProvider = tester.PayTester.GetService<PayJoinStateProvider>();
var btcPayNetwork = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(btcPayNetwork);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var senderUser = tester.NewAccount();
senderUser.GrantAccess(true);
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
var invoice = senderUser.BitPay.CreateInvoice(
new Invoice() {Price = 100, Currency = "USD", FullNotifications = true});
//payjoin is not enabled by default.
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}", invoice.CryptoInfo.First().PaymentUrls.BIP21);
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
Money.Coins(0.06m));
var receiverUser = tester.NewAccount();
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
await receiverUser.EnablePayJoin();
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
invoice = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
Money.Coins(0.06m));
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
//give the cow some cash
await cashCow.GenerateAsync(1);
//let's get some more utxos first
await receiverUser.ReceiveUTXO(Money.Coins(0.011m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.012m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.013m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.014m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.021m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.022m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.023m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.024m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.025m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.026m), btcPayNetwork);
var senderChange = await senderUser.GetNewAddress(btcPayNetwork);
//Let's start the harassment
invoice = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice2 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var senderStore = await tester.PayTester.StoreRepository.FindStore(senderUser.StoreId);
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var derivationSchemeSettings = senderStore.GetSupportedPaymentMethods(tester.NetworkProvider)
.OfType<DerivationSchemeSettings>().SingleOrDefault(settings =>
settings.PaymentId == paymentMethodId);
ReceivedCoin[] senderCoins = null;
await TestUtils.EventuallyAsync(async () =>
{
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
});
var coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
var coin2 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
var coin3 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
var coin4 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
var coin5 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
var coin6 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
signingKeySettings.RootFingerprint =
senderUser.GenerateWalletResponseV.MasterHDKey.GetPublicKey().GetHDFingerPrint();
var extKey =
senderUser.GenerateWalletResponseV.MasterHDKey.Derive(signingKeySettings.GetRootedKeyPath()
.KeyPath);
var n = tester.ExplorerClient.Network.NBitcoinNetwork;
var Invoice1Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(parsedBip21.Address, parsedBip21.Amount)
.AddCoins(coin.Coin)
.AddKeys(extKey.Derive(coin.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
var Invoice1Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(parsedBip21.Address, parsedBip21.Amount)
.AddCoins(coin2.Coin)
.AddKeys(extKey.Derive(coin2.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
var Invoice2Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(secondInvoiceParsedBip21.Address, secondInvoiceParsedBip21.Amount)
.AddCoins(coin.Coin)
.AddKeys(extKey.Derive(coin.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
var Invoice2Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(secondInvoiceParsedBip21.Address, secondInvoiceParsedBip21.Amount)
.AddCoins(coin2.Coin)
.AddKeys(extKey.Derive(coin2.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
//Attempt 1: Send a signed tx to invoice 1 that does not pay the invoice at all
//Result: reject
// Assert.False((await tester.PayTester.HttpClient.PostAsync(endpoint,
// new StringContent(Invoice2Coin1.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode);
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
//Result: Second Tx should be rejected.
var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork);
await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid");
var contributedInputsInvoice1Coin1ResponseTx =
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);
Assert.Single(contributedInputsInvoice1Coin1ResponseTx);
//Attempt 3: Send the same inputs from invoice 1 to invoice 2 while invoice 1 tx has not been broadcasted
//Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, "inputs-already-used");
var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork);
var contributedInputsInvoice2Coin2ResponseTx =
Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut);
Assert.Single(contributedInputsInvoice2Coin2ResponseTx);
//Attempt 4: Make tx that pays invoice 3 and 4 and submit to both
//Result: reject on 4: the protocol should not worry about this complexity
var invoice3 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice4 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var Invoice3AndInvoice4Coin3 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(invoice3ParsedBip21.Address, invoice3ParsedBip21.Amount)
.Send(invoice4ParsedBip21.Address, invoice4ParsedBip21.Amount)
.AddCoins(coin3.Coin)
.AddKeys(extKey.Derive(coin3.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
await senderUser.SubmitPayjoin(invoice3, Invoice3AndInvoice4Coin3, btcPayNetwork);
await senderUser.SubmitPayjoin(invoice4, Invoice3AndInvoice4Coin3, btcPayNetwork, "already-paid");
//Attempt 5: Make tx that pays invoice 5 with 2 outputs
//Result: proposed tx consolidates the outputs
var invoice5 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var Invoice5Coin4TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(invoice5ParsedBip21.Address, invoice5ParsedBip21.Amount / 2)
.Send(invoice5ParsedBip21.Address, invoice5ParsedBip21.Amount / 2)
.AddCoins(coin4.Coin)
.AddKeys(extKey.Derive(coin4.KeyPath))
.SendEstimatedFees(new FeeRate(100m));
var Invoice5Coin4 = Invoice5Coin4TxBuilder.BuildTransaction(true);
var Invoice5Coin4ResponseTx = await senderUser.SubmitPayjoin(invoice5, Invoice5Coin4, btcPayNetwork);
Assert.Single(Invoice5Coin4ResponseTx.Outputs.To(invoice5ParsedBip21.Address));
//Attempt 10: send tx with rbf, broadcast payjoin tx, bump the rbf payjoin , attempt to submit tx again
//Result: same tx gets sent back
//give the receiver some more utxos
Assert.NotNull(await tester.ExplorerNode.SendToAddressAsync(
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
new Money(0.1m, MoneyUnit.BTC)));
var invoice6 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice6Coin5TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(invoice6ParsedBip21.Address, invoice6ParsedBip21.Amount)
.AddCoins(coin5.Coin)
.AddKeys(extKey.Derive(coin5.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.SetLockTime(0);
var invoice6Coin5 = invoice6Coin5TxBuilder
.BuildTransaction(true);
var Invoice6Coin5Response1Tx =await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx);
//broadcast the first payjoin
await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned);
// invoice6Coin5TxBuilder = invoice6Coin5TxBuilder.SendEstimatedFees(new FeeRate(100m));
// var invoice6Coin5Bumpedfee = invoice6Coin5TxBuilder
// .BuildTransaction(true);
//
// var Invoice6Coin5Response3 = await tester.PayTester.HttpClient.PostAsync(invoice6Endpoint,
// new StringContent(invoice6Coin5Bumpedfee.ToHex(), Encoding.UTF8, "text/plain"));
// Assert.True(Invoice6Coin5Response3.IsSuccessStatusCode);
// var Invoice6Coin5Response3Tx =
// Transaction.Parse(await Invoice6Coin5Response3.Content.ReadAsStringAsync(), n);
// Assert.True(invoice6Coin5Bumpedfee.Inputs.All(txin =>
// Invoice6Coin5Response3Tx.Inputs.Any(txin2 => txin2.PrevOut == txin.PrevOut)));
//Attempt 11:
//send tx with rbt, broadcast payjoin,
//create tx spending the original tx inputs with rbf to self,
//Result: the exposed utxos are priorized in the next p2ep
//give the receiver some more utxos
Assert.NotNull(await tester.ExplorerNode.SendToAddressAsync(
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
new Money(0.1m, MoneyUnit.BTC)));
var invoice7 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice7Coin6TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(invoice7ParsedBip21.Address, invoice7ParsedBip21.Amount)
.AddCoins(coin6.Coin)
.AddKeys(extKey.Derive(coin6.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.SetLockTime(0);
var invoice7Coin6Tx = invoice7Coin6TxBuilder
.BuildTransaction(true);
var invoice7Coin6Response1Tx = await senderUser.SubmitPayjoin(invoice7, invoice7Coin6Tx, btcPayNetwork);
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
var contributedInputsInvoice7Coin6Response1TxSigned =
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id);
//broadcast the payjoin
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
Assert.True(res.Success);
// Paid with coinjoin
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatus.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
});
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
var invoice7Coin6Tx2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.AddCoins(coin6.Coin)
.SendAll(senderChange)
.SubtractFees()
.AddKeys(extKey.Derive(coin6.KeyPath))
.SendEstimatedFees(new FeeRate(200m))
.SetLockTime(0)
.BuildTransaction(true);
//broadcast the "rbf cancel" tx
res = (await tester.ExplorerClient.BroadcastAsync(invoice7Coin6Tx2));
Assert.True(res.Success);
// Make a block, this should put back the invoice to new
var blockhash = tester.ExplorerNode.Generate(1)[0];
Assert.NotNull(await tester.ExplorerNode.GetRawTransactionAsync(invoice7Coin6Tx2.GetHash(), blockhash));
Assert.Null(await tester.ExplorerNode.GetRawTransactionAsync(Invoice7Coin6Response1TxSigned.GetHash(), blockhash, false));
// Now we should return to New
OutPoint ourOutpoint = null;
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatus.New, invoiceEntity.Status);
Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0];
});
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
// The outpoint should now be available for next pj selection
Assert.False(await payjoinRepository.TryUnlock(ourOutpoint));
}
}
}
}

@ -6,10 +6,12 @@ using BTCPayServer.Data;
using BTCPayServer.Services.Rates;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using Logs = BTCPayServer.Tests.Logging.Logs;
namespace BTCPayServer.Tests
{
@ -41,8 +43,8 @@ namespace BTCPayServer.Tests
currencyPairRateResult.Add(new CurrencyPair("USD", "BTC"), Task.FromResult(rateResultUSDBTC));
currencyPairRateResult.Add(new CurrencyPair("BTC", "USD"), Task.FromResult(rateResultBTCUSD));
handlerBTC = new BitcoinLikePaymentHandler(null, networkProvider, null, null);
InvoiceLogs logs = new InvoiceLogs();
handlerBTC = new BitcoinLikePaymentHandler(null, networkProvider, null, null, null);
handlerLN = new LightningLikePaymentHandler(null, null, networkProvider, null);
#pragma warning restore CS0618

@ -19,6 +19,7 @@ using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Stores;
using Newtonsoft.Json;
@ -32,9 +33,9 @@ namespace BTCPayServer.Tests
public IWebDriver Driver { get; set; }
public ServerTester Server { get; set; }
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null)
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false)
{
var server = ServerTester.Create(scope);
var server = ServerTester.Create(scope, newDb);
return new SeleniumTester()
{
Server = server
@ -120,25 +121,24 @@ namespace BTCPayServer.Tests
return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value"));
}
public string GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false)
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false)
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id("import-from-btn")).ForceClick();
Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick();
Thread.Sleep(200); // allow for modal to fade in
Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed);
SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys);
SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys);
Driver.FindElement(By.Id("btn-generate")).ForceClick();
SetCheckbox(Driver.WaitForElement(By.Id("SavePrivateKeys")), privkeys);
SetCheckbox(Driver.WaitForElement(By.Id("ImportKeysToRPC")), importkeys);
Logs.Tester.LogInformation("Trying to click btn-generate");
Driver.WaitForElement(By.Id("btn-generate")).ForceClick();
AssertHappyMessage();
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text;
}
Driver.FindElement(By.Id("Confirm")).ForceClick();
AssertHappyMessage();
return seed;
return new Mnemonic(seed);
}
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
@ -254,13 +254,14 @@ namespace BTCPayServer.Tests
if (value != element.Selected)
{
Logs.Tester.LogInformation("SetCheckbox recursion, trying to click again");
SetCheckbox(element, value);
}
}
public void SetCheckbox(SeleniumTester s, string inputName, bool value)
public void SetCheckbox(SeleniumTester s, string checkboxId, bool value)
{
SetCheckbox(s.Driver.FindElement(By.Name(inputName)), value);
SetCheckbox(s.Driver.WaitForElement(By.Id(checkboxId)), value);
}
public void ScrollToElement(IWebElement element)
@ -313,8 +314,38 @@ namespace BTCPayServer.Tests
return id;
}
public async Task FundStoreWallet(WalletId walletId, int coins = 1, decimal denomination = 1m)
{
GoToWalletReceive(walletId);
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("vue-address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (int i = 0; i < coins; i++)
{
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
}
}
public void PayInvoice(WalletId walletId, string invoiceId)
{
GoToInvoiceCheckout(invoiceId);
var bip21 = Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
GoToWalletSend(walletId);
Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
Driver.ScrollTo(By.Id("SendMenu"));
Driver.FindElement(By.Id("SendMenu")).ForceClick();
Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
}
private void CheckForJSErrors()
{
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
@ -338,6 +369,14 @@ namespace BTCPayServer.Tests
}
public void GoToWalletSend(WalletId walletId)
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/send"));
}
internal void GoToWalletReceive(WalletId walletId)
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/receive"));
}
}
}

@ -11,6 +11,9 @@ using System.Threading.Tasks;
using System.Text.RegularExpressions;
using BTCPayServer.Models;
using NBitcoin.Payment;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Tests
{
@ -417,7 +420,68 @@ namespace BTCPayServer.Tests
s.Driver.Quit();
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseCoinSelection()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var userId = s.RegisterNewUser(true);
var storeId = s.CreateNewStore().storeId;
s.GenerateWallet("BTC", "", false, true);
var walletId = new WalletId(storeId, "BTC");
s.GoToWalletReceive(walletId);
s.Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = s.Driver.FindElement(By.Id("vue-address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
await s.Server.ExplorerNode.GenerateAsync(1);
for (int i = 0; i < 6; i++)
{
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
}
var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
var spentOutpoint = new OutPoint(targetTx, tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
await TestUtils.EventuallyAsync(async () =>
{
var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode);
Assert.Contains(
await s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode)
.GetUnspentCoins(x.AccountDerivation),
coin => coin.OutPoint == spentOutpoint);
});
await s.Server.ExplorerNode.GenerateAsync(1);
s.GoToWalletSend(walletId);
s.Driver.FindElement(By.Id("advancedSettings")).Click();
s.Driver.FindElement(By.Id("toggleInputSelection")).Click();
s.Driver.WaitForElement(By.Id(spentOutpoint.ToString()));
Assert.Equal("true", s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant());
var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click();
var inputSelectionSelect = s.Driver.FindElement(By.Name("SelectedInputs"));
Assert.Single(inputSelectionSelect.FindElements(By.CssSelector("[selected]")));
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 0.3m);
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
var happyElement = s.AssertHappyMessage();
var happyText = happyElement.Text;
var txid = Regex.Match(happyText, @"\((.*)\)").Groups[1].Value;
tx = await s.Server.ExplorerNode.GetRawTransactionAsync(new uint256(txid));
Assert.Single(tx.Inputs);
Assert.Equal(spentOutpoint, tx.Inputs[0].PrevOut);
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanManageWallet()
{
@ -488,7 +552,7 @@ namespace BTCPayServer.Tests
var mnemonic = s.GenerateWallet("BTC", "", true, true);
//lets import and save private keys
var root = new Mnemonic(mnemonic).DeriveExtKey();
var root = mnemonic.DeriveExtKey();
invoiceId = s.CreateInvoice(storeId.storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
@ -518,18 +582,18 @@ namespace BTCPayServer.Tests
Assert.Contains(tx.ToString(), s.Driver.PageSource);
void SignWith(string signingSource)
void SignWith(Mnemonic signingSource)
{
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(0, bob, 1);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
// Input the seed
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource.ToString() + Keys.Enter);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
@ -537,19 +601,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
Assert.Equal(walletTransactionLink, s.Driver.Url);
}
void SetTransactionOutput(int index, BitcoinAddress dest, decimal amount, bool subtract = false)
{
s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString());
var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount"));
amountElement.Clear();
amountElement.SendKeys(amount.ToString());
var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput"));
if (checkboxElement.Selected != subtract)
{
checkboxElement.Click();
}
}
SignWith(mnemonic);
@ -558,7 +609,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("WalletSend")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(0, jack, 0.01m);
SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
@ -589,5 +640,17 @@ namespace BTCPayServer.Tests
}
}
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)
{
s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString());
var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount"));
amountElement.Clear();
amountElement.SendKeys(amount.ToString());
var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput"));
if (checkboxElement.Selected != subtract)
{
checkboxElement.Click();
}
}
}
}

@ -29,13 +29,13 @@ namespace BTCPayServer.Tests
{
public class ServerTester : IDisposable
{
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
public static ServerTester Create([CallerMemberNameAttribute]string scope = null, bool newDb = false)
{
return new ServerTester(scope);
return new ServerTester(scope, newDb);
}
string _Directory;
public ServerTester(string scope)
public ServerTester(string scope, bool newDb)
{
_Directory = scope;
if (Directory.Exists(_Directory))
@ -59,6 +59,12 @@ namespace BTCPayServer.Tests
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
};
if (newDb)
{
var r = RandomUtils.GetUInt32();
PayTester.Postgres = PayTester.Postgres.Replace("btcpayserver", $"btcpayserver{r}");
PayTester.MySQL = PayTester.MySQL.Replace("btcpayserver", $"btcpayserver{r}");
}
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
@ -66,6 +72,7 @@ namespace BTCPayServer.Tests
PayTester.SSHPassword = GetEnvironment("TESTS_SSHPASSWORD", "opD3i2282D");
PayTester.SSHKeyFile = GetEnvironment("TESTS_SSHKEYFILE", "");
PayTester.SSHConnection = GetEnvironment("TESTS_SSHCONNECTION", "root@127.0.0.1:21622");
PayTester.SocksEndpoint = GetEnvironment("TESTS_SOCKSENDPOINT", "localhost:9050");
}
public void ActivateLTC()
@ -138,6 +145,19 @@ namespace BTCPayServer.Tests
await CustomerLightningD.Pay(bolt11);
}
public async Task<T> WaitForEvent<T>(Func<Task> action)
{
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
var sub = PayTester.GetService<EventAggregator>().Subscribe<T>(evt =>
{
tcs.TrySetResult(evt);
});
await action.Invoke();
var result = await tcs.Task;
sub.Dispose();
return result;
}
public ILightningClient CustomerLightningD { get; set; }
public ILightningClient MerchantLightningD { get; private set; }

@ -8,8 +8,10 @@ using NBitcoin;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Amazon.S3.Model;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
@ -20,44 +22,88 @@ using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Identity;
using NBXplorer.Models;
using BTCPayServer.Client;
using BTCPayServer.Events;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using NBitcoin.Payment;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Tests
{
public class TestAccount
{
ServerTester parent;
public TestAccount(ServerTester parent)
{
this.parent = parent;
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
}
public void GrantAccess()
public void GrantAccess(bool isAdmin = false)
{
GrantAccessAsync().GetAwaiter().GetResult();
GrantAccessAsync(isAdmin).GetAwaiter().GetResult();
}
public async Task MakeAdmin()
public async Task MakeAdmin(bool isAdmin = true)
{
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
var u = await userManager.FindByIdAsync(UserId);
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
if (isAdmin)
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
else
await userManager.RemoveFromRoleAsync(u, Roles.ServerAdmin);
IsAdmin = true;
}
public void Register()
public Task<BTCPayServerClient> CreateClient()
{
RegisterAsync().GetAwaiter().GetResult();
return Task.FromResult(new BTCPayServerClient(parent.PayTester.ServerUri, RegisterDetails.Email,
RegisterDetails.Password));
}
public async Task GrantAccessAsync()
public async Task<BTCPayServerClient> CreateClient(params string[] permissions)
{
await RegisterAsync();
var manageController = parent.PayTester.GetController<ManageController>(UserId, StoreId, IsAdmin);
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
new ManageController.AddApiKeyViewModel()
{
PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem()
{
Permission = s,
Value = true
}).ToList()
}));
var statusMessage = manageController.TempData.GetStatusMessageModel();
Assert.NotNull(statusMessage);
var str = "<code class='alert-link'>";
var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf(str) + str.Length);
apiKey = apiKey.Substring(0, apiKey.IndexOf("</code>"));
return new BTCPayServerClient(parent.PayTester.ServerUri, apiKey);
}
public void Register(bool isAdmin = false)
{
RegisterAsync(isAdmin).GetAwaiter().GetResult();
}
public async Task GrantAccessAsync(bool isAdmin = false)
{
await RegisterAsync(isAdmin);
await CreateStoreAsync();
var store = this.GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public BTCPayServerClient CreateClientFromAPIKey(string apiKey)
{
return new BTCPayServerClient(parent.PayTester.ServerUri, apiKey);
}
public void CreateStore()
{
CreateStoreAsync().GetAwaiter().GetResult();
@ -70,6 +116,7 @@ namespace BTCPayServer.Tests
store.NetworkFeeMode = mode;
});
}
public void ModifyStore(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
@ -87,44 +134,60 @@ namespace BTCPayServer.Tests
public async Task CreateStoreAsync()
{
var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
await store.CreateStore(new CreateStoreViewModel() {Name = "Test Store"});
StoreId = store.CreatedStoreId;
parent.Stores.Add(StoreId);
}
public BTCPayNetwork SupportedNetwork { get; set; }
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false, bool importKeysToNBX = false)
public WalletId RegisterDerivationScheme(string crytoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy, bool importKeysToNBX = false)
{
return RegisterDerivationSchemeAsync(crytoCode, segwit, importKeysToNBX).GetAwaiter().GetResult();
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false, bool importKeysToNBX = false)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
bool importKeysToNBX = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
{
ScriptPubKeyType = segwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy,
SavePrivateKeys = importKeysToNBX
ScriptPubKeyType = segwit,
SavePrivateKeys = importKeysToNBX,
});
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
Enabled = true,
CryptoCode = cryptoCode,
Network = SupportedNetwork,
RootFingerprint = GenerateWalletResponseV.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = SupportedNetwork.GetRootKeyPath(),
Source = "NBXplorer",
AccountKey = GenerateWalletResponseV.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
await store.AddDerivationScheme(StoreId,
new DerivationSchemeViewModel()
{
Enabled = true,
CryptoCode = cryptoCode,
Network = SupportedNetwork,
RootFingerprint = GenerateWalletResponseV.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = SupportedNetwork.GetRootKeyPath(),
Source = "NBXplorer",
AccountKey = GenerateWalletResponseV.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public async Task EnablePayJoin()
{
var storeController = parent.PayTester.GetController<StoresController>(UserId, StoreId);
var storeVM =
Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(storeController.UpdateStore()).Model);
storeVM.PayJoinEnabled = true;
Assert.Equal(nameof(storeController.UpdateStore),
Assert.IsType<RedirectToActionResult>(
await storeController.UpdateStore(storeVM)).ActionName);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
public DerivationStrategyBase DerivationScheme
@ -135,7 +198,7 @@ namespace BTCPayServer.Tests
}
}
private async Task RegisterAsync()
private async Task RegisterAsync(bool isAdmin = false)
{
var account = parent.PayTester.GetController<AccountController>();
RegisterDetails = new RegisterViewModel()
@ -143,27 +206,33 @@ namespace BTCPayServer.Tests
Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
IsAdmin = isAdmin
};
await account.Register(RegisterDetails);
UserId = account.RegisteredUserId;
IsAdmin = account.RegisteredAdmin;
}
public RegisterViewModel RegisterDetails{ get; set; }
public RegisterViewModel RegisterDetails { get; set; }
public Bitpay BitPay
{
get; set;
get;
set;
}
public string UserId
{
get; set;
get;
set;
}
public string StoreId
{
get; set;
get;
set;
}
public bool IsAdmin { get; internal set; }
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
@ -179,19 +248,145 @@ namespace BTCPayServer.Tests
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException(connectionType.ToString());
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
ConnectionString = connectionString,
SkipPortTest = true
}, "save", "BTC");
await storeController.AddLightningNode(StoreId,
new LightningNodeViewModel() {ConnectionString = connectionString, SkipPortTest = true}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network)
{
var cashCow = parent.ExplorerNode;
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
await parent.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
await cashCow.SendToAddressAsync(address, value);
});
int i = 0;
while (i <30)
{
var result = (await btcPayWallet.GetUnspentCoins(DerivationScheme))
.FirstOrDefault(c => c.ScriptPubKey == address.ScriptPubKey)?.Coin;
if (result != null)
{
return result;
}
await Task.Delay(1000);
i++;
}
Assert.False(true);
return null;
}
public async Task<BitcoinAddress> GetNewAddress(BTCPayNetwork network)
{
var cashCow = parent.ExplorerNode;
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
return address;
}
public async Task<PSBT> Sign(PSBT psbt)
{
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>()
.GetWallet(psbt.Network.NetworkSet.CryptoCode);
var explorerClient = parent.PayTester.GetService<ExplorerClientProvider>()
.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode);
psbt = (await explorerClient.UpdatePSBTAsync(new UpdatePSBTRequest()
{
DerivationScheme = DerivationScheme, PSBT = psbt
})).PSBT;
return psbt.SignAll(this.DerivationScheme, GenerateWalletResponseV.AccountHDKey,
GenerateWalletResponseV.AccountKeyPath);
}
public async Task<PSBT> SubmitPayjoin(Invoice invoice, PSBT psbt, string expectedError = null, bool senderError= false)
{
var endpoint = GetPayjoinEndpoint(invoice, psbt.Network);
if (endpoint == null)
{
return null;
}
var pjClient = parent.PayTester.GetService<PayjoinClient>();
var storeRepository = parent.PayTester.GetService<StoreRepository>();
var store = await storeRepository.FindStore(StoreId);
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>()
.First();
Logs.Tester.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
if (expectedError is null && !senderError)
{
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
Logs.Tester.LogInformation($"Proposed payjoin is {proposed.GetGlobalTransaction().GetHash()}");
Assert.NotNull(proposed);
return proposed;
}
else
{
if (senderError)
{
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
}
else
{
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
Assert.Equal(expectedError, ex.ErrorCode);
}
return null;
}
}
public async Task<Transaction> SubmitPayjoin(Invoice invoice, Transaction transaction, BTCPayNetwork network,
string expectedError = null)
{
var response =
await SubmitPayjoinCore(transaction.ToHex(), invoice, network.NBitcoinNetwork, expectedError);
if (response == null)
return null;
var signed = Transaction.Parse(await response.Content.ReadAsStringAsync(), network.NBitcoinNetwork);
return signed;
}
async Task<HttpResponseMessage> SubmitPayjoinCore(string content, Invoice invoice, Network network,
string expectedError)
{
var endpoint = GetPayjoinEndpoint(invoice, network);
var response = await parent.PayTester.HttpClient.PostAsync(endpoint,
new StringContent(content, Encoding.UTF8, "text/plain"));
if (expectedError != null)
{
Assert.False(response.IsSuccessStatusCode);
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(expectedError, error["errorCode"].Value<string>());
return null;
}
else
{
if (!response.IsSuccessStatusCode)
{
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
Assert.True(false,
$"Error: {error["errorCode"].Value<string>()}: {error["message"].Value<string>()}");
}
}
return response;
}
private static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
{
var parsedBip21 = new BitcoinUrlBuilder(
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
network);
return parsedBip21.UnknowParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null;
}
}
}

@ -9,6 +9,7 @@ using Xunit.Sdk;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace BTCPayServer.Tests
{
@ -41,6 +42,12 @@ namespace BTCPayServer.Tests
return Path.Combine(directory.FullName, "TestData", relativeFilePath);
}
public static T AssertType<T>(this object obj)
{
Assert.IsType<T>(obj);
return (T)obj;
}
public static FormFile GetFormFile(string filename, string content)
{
File.WriteAllText(filename, content);

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
#!/bin/bash
bitcoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)"
address=$(docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" getnewaddress)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" generatetoaddress "$@" "$address"

@ -3,26 +3,26 @@ version: "3"
services:
monerod:
image: kukks/docker-monero:test
image: btcpayserver/monero:0.15.0.1-amd64
restart: unless-stopped
container_name: xmr_monerod
entrypoint: monerod --fixed-difficulty 100 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --non-interactive --block-notify="/scripts/notifier.sh https://127.0.0.1:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --no-sync --offline
entrypoint: sleep 999999
# entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
monero_wallet:
image: kukks/docker-monero:test
image: btcpayserver/monero:0.15.0.1-amd64
restart: unless-stopped
container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=127.0.0.1:18081 --wallet-file=/wallet/wallet.keys --tx-notify="/scripts/notifier.sh https://127.0.0.1:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
ports:
- "18082:18082"
volumes:
- "monero_wallet:/wallet"
- "./monero_wallet:/wallet"
depends_on:
- monerod
volumes:
monero_data:
monero_wallet:

@ -27,6 +27,7 @@ services:
TESTS_SSHCONNECTION: "root@sshd:22"
TESTS_SSHPASSWORD: ""
TESTS_SSHKEYFILE: ""
TESTS_SOCKSENDPOINT: "tor:9050"
expose:
- "80"
links:
@ -51,6 +52,7 @@ services:
- customer_lnd
- merchant_lnd
- sshd
- tor
sshd:
build:
@ -76,7 +78,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.8
image: nicolasdorier/nbxplorer:2.1.24
restart: unless-stopped
ports:
- "32838:32838"
@ -319,6 +321,21 @@ services:
links:
- bitcoind
tor:
restart: unless-stopped
image: btcpayserver/tor:0.4.1.5
container_name: tor
environment:
TOR_PASSWORD: btcpayserver
ports:
- "9050:9050" # SOCKS
- "9051:9051" # Tor Control
volumes:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
volumes:
sshd_datadir:
bitcoin_datadir:
@ -328,3 +345,6 @@ volumes:
lightning_charge_datadir:
customer_lnd_datadir:
merchant_lnd_datadir:
tor_datadir:
torrcdir:
tor_servicesdir:

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<PropertyGroup Condition="'$(Configuration)' == 'Debug' And '$(RazorCompileOnBuild)' != 'true'">
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<DefineConstants>$(DefineConstants);RAZOR_RUNTIME_COMPILE</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
@ -30,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.8" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.9" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
@ -41,14 +42,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitpayClient" Version="1.0.0.35" />
<PackageReference Include="NBitpayClient" Version="1.0.0.38" />
<PackageReference Include="DBriize" Version="1.0.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<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.1.0" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="NSwag.AspNetCore" Version="13.2.2" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
@ -67,7 +67,7 @@
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(RazorCompileOnBuild)' != 'true'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
</ItemGroup>
@ -121,9 +121,11 @@
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
<Folder Include="wwwroot\vendor\vue-qrcode-reader" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
@ -206,8 +208,6 @@
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Remove="Views\Server\EditGoogleCloudStorageStorageProvider.cshtml">
</Content>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -222,4 +222,6 @@
<ItemGroup>
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

@ -23,6 +23,7 @@ using BTCPayServer.U2F.Models;
using Newtonsoft.Json;
using NicolasDorier.RateLimits;
using BTCPayServer.Data;
using BTCPayServer.Events;
using U2F.Core.Exceptions;
namespace BTCPayServer.Controllers
@ -40,6 +41,7 @@ namespace BTCPayServer.Controllers
Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
public U2FService _u2FService;
private readonly EventAggregator _eventAggregator;
ILogger _logger;
public AccountController(
@ -51,7 +53,8 @@ namespace BTCPayServer.Controllers
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService)
U2FService u2FService,
EventAggregator eventAggregator)
{
this.storeRepository = storeRepository;
_userManager = userManager;
@ -62,6 +65,7 @@ namespace BTCPayServer.Controllers
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_eventAggregator = eventAggregator;
_logger = Logs.PayServer;
}
@ -400,6 +404,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[AllowAnonymous]
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true, bool useBasicLayout = false)
{
if (!CanLoginOrRegister())
@ -439,7 +444,6 @@ namespace BTCPayServer.Controllers
if (result.Succeeded)
{
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration))
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
@ -450,17 +454,21 @@ namespace BTCPayServer.Controllers
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).
Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)");
policies.LockSubscription = true;
await _SettingsRepository.UpdateSetting(policies);
}
RegisteredAdmin = true;
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
_eventAggregator.Publish(new UserRegisteredEvent()
{
RequestUri = Request.GetAbsoluteRootUri(),
User = user,
Admin = RegisteredAdmin
});
RegisteredUserId = user.Id;
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
if (!policies.RequiresConfirmedEmail)
{
if (logon)

@ -83,7 +83,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(ListApps));
@ -103,7 +103,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(ListApps));

@ -237,6 +237,10 @@ namespace BTCPayServer.Controllers
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
if (request.Amount <= 0)
{
return NotFound("Please provide an amount greater than 0");
}
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)

@ -0,0 +1,94 @@
using System.Threading.Tasks;
using System.Linq;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Security.GreenField;
using NBitcoin.DataEncoders;
using NBitcoin;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
public class ApiKeysController : ControllerBase
{
private readonly APIKeyRepository _apiKeyRepository;
private readonly UserManager<ApplicationUser> _userManager;
public ApiKeysController(APIKeyRepository apiKeyRepository, UserManager<ApplicationUser> userManager)
{
_apiKeyRepository = apiKeyRepository;
_userManager = userManager;
}
[HttpGet("~/api/v1/api-keys/current")]
public async Task<ActionResult<ApiKeyData>> GetKey()
{
if (!ControllerContext.HttpContext.GetAPIKey(out var apiKey))
{
return NotFound();
}
var data = await _apiKeyRepository.GetKey(apiKey);
return Ok(FromModel(data));
}
[HttpPost("~/api/v1/api-keys")]
[Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<ActionResult<ApiKeyData>> CreateKey(CreateApiKeyRequest request)
{
if (request is null)
return BadRequest();
var key = new APIKeyData()
{
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
Type = APIKeyType.Permanent,
UserId = _userManager.GetUserId(User),
Label = request.Label
};
key.SetBlob(new APIKeyBlob()
{
Permissions = request.Permissions.Select(p => p.ToString()).Distinct().ToArray()
});
await _apiKeyRepository.CreateKey(key);
return Ok(FromModel(key));
}
[HttpDelete("~/api/v1/api-keys/current")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
public Task<IActionResult> RevokeCurrentKey()
{
if (!ControllerContext.HttpContext.GetAPIKey(out var apiKey))
{
// Should be impossible (we force apikey auth)
return Task.FromResult<IActionResult>(BadRequest());
}
return RevokeKey(apiKey);
}
[HttpDelete("~/api/v1/api-keys/{apikey}", Order = 1)]
[Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> RevokeKey(string apikey)
{
if (string.IsNullOrEmpty(apikey))
return BadRequest();
if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
return Ok();
else
return NotFound();
}
private static ApiKeyData FromModel(APIKeyData data)
{
return new ApiKeyData()
{
Permissions = Permission.ToPermissions(data.GetBlob().Permissions).ToArray(),
ApiKey = data.Id,
Label = data.Label ?? string.Empty
};
}
}
}

@ -1,20 +1,20 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Security;
using BTCPayServer.Security.APIKeys;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.RestApi
namespace BTCPayServer.Controllers.GreenField
{
/// <summary>
/// this controller serves as a testing endpoint for our api key unit tests
/// </summary>
[Route("api/test/apikey")]
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class TestApiKeyController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
@ -27,45 +27,45 @@ namespace BTCPayServer.Controllers.RestApi
}
[HttpGet("me/id")]
[Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public string GetCurrentUserId()
{
return _userManager.GetUserId(User);
}
[HttpGet("me")]
[Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<ApplicationUser> GetCurrentUser()
{
return await _userManager.GetUserAsync(User);
}
[HttpGet("me/is-admin")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public bool AmIAnAdmin()
{
return true;
}
[HttpGet("me/stores")]
[Authorize(Policy = Policies.CanListStoreSettings.Key,
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public async Task<StoreData[]> GetCurrentUserStores()
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public StoreData[] GetCurrentUserStores()
{
return await User.GetStores(_userManager, _storeRepository);
return this.HttpContext.GetStoresData();
}
[HttpGet("me/stores/actions")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public bool CanDoNonImplicitStoreActions()
[HttpGet("me/stores/{storeId}/can-view")]
[Authorize(Policy = Policies.CanViewStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public bool CanViewStore(string storeId)
{
return true;
}
[HttpGet("me/stores/{storeId}/can-edit")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public bool CanEdit(string storeId)
[Authorize(Policy = Policies.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public bool CanEditStore(string storeId)
{
return true;
}

@ -0,0 +1,173 @@
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Security;
using BTCPayServer.Security.GreenField;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NicolasDorier.RateLimits;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class UsersController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayServerOptions _btcPayServerOptions;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly SettingsRepository _settingsRepository;
private readonly EventAggregator _eventAggregator;
private readonly IPasswordValidator<ApplicationUser> _passwordValidator;
private readonly RateLimitService _throttleService;
private readonly BTCPayServerOptions _options;
private readonly IAuthorizationService _authorizationService;
public UsersController(UserManager<ApplicationUser> userManager, BTCPayServerOptions btcPayServerOptions,
RoleManager<IdentityRole> roleManager, SettingsRepository settingsRepository,
EventAggregator eventAggregator,
IPasswordValidator<ApplicationUser> passwordValidator,
RateLimitService throttleService,
BTCPayServerOptions options,
IAuthorizationService authorizationService)
{
_userManager = userManager;
_btcPayServerOptions = btcPayServerOptions;
_roleManager = roleManager;
_settingsRepository = settingsRepository;
_eventAggregator = eventAggregator;
_passwordValidator = passwordValidator;
_throttleService = throttleService;
_options = options;
_authorizationService = authorizationService;
}
[Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/users/me")]
public async Task<ActionResult<ApplicationUserData>> GetCurrentUser()
{
var user = await _userManager.GetUserAsync(User);
return FromModel(user);
}
[AllowAnonymous]
[HttpPost("~/api/v1/users")]
public async Task<ActionResult<ApplicationUserData>> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
{
if (request?.Email is null)
return BadRequest(CreateValidationProblem(nameof(request.Email), "Email is missing"));
if (!Validation.EmailValidator.IsEmail(request.Email))
{
return BadRequest(CreateValidationProblem(nameof(request.Email), "Invalid email"));
}
if (request?.Password is null)
return BadRequest(CreateValidationProblem(nameof(request.Password), "Password is missing"));
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
var isAuth = User.Identity.AuthenticationType == GreenFieldConstants.AuthenticationType;
// If registration are locked and that an admin exists, don't accept unauthenticated connection
if (anyAdmin && policies.LockSubscription && !isAuth)
return Unauthorized();
// Even if subscription are unlocked, it is forbidden to create admin unauthenticated
if (anyAdmin && request.IsAdministrator is true && !isAuth)
return Forbid(AuthenticationSchemes.GreenfieldBasic);
// You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements
bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded
&& (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded
&& isAuth
: true;
// You need to be admin to create an admin
if (request.IsAdministrator is true && !isAdmin)
return Forbid(AuthenticationSchemes.GreenfieldBasic);
if (!isAdmin && policies.LockSubscription)
{
// If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission
var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded;
if (!isAuth || !canCreateUser)
return Forbid(AuthenticationSchemes.GreenfieldBasic);
}
var user = new ApplicationUser
{
UserName = request.Email,
Email = request.Email,
RequiresEmailConfirmation = policies.RequiresConfirmedEmail
};
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
if (!passwordValidation.Succeeded)
{
foreach (var error in passwordValidation.Errors)
{
ModelState.AddModelError(nameof(request.Password), error.Description);
}
return BadRequest(new ValidationProblemDetails(ModelState));
}
if (!isAdmin)
{
if (!await _throttleService.Throttle(ZoneLimits.Register, this.HttpContext.Connection.RemoteIpAddress, cancellationToken))
return new TooManyRequestsResult(ZoneLimits.Register);
}
var identityResult = await _userManager.CreateAsync(user, request.Password);
if (!identityResult.Succeeded)
{
foreach (var error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return BadRequest(new ValidationProblemDetails(ModelState));
}
if (request.IsAdministrator is true)
{
if (!anyAdmin)
{
await _roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
}
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
if (!anyAdmin)
{
if (_options.DisableRegistration)
{
// automatically lock subscriptions now that we have our first admin
Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)");
policies.LockSubscription = true;
await _settingsRepository.UpdateSetting(policies);
}
}
}
_eventAggregator.Publish(new UserRegisteredEvent() {RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true });
return CreatedAtAction(string.Empty, user);
}
private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage)
{
var modelState = new ModelStateDictionary();
modelState.AddModelError(propertyName, errorMessage);
return new ValidationProblemDetails(modelState);
}
private static ApplicationUserData FromModel(ApplicationUser data)
{
return new ApplicationUserData()
{
Id = data.Id,
Email = data.Email,
EmailConfirmed = data.EmailConfirmed,
RequiresEmailConfirmation = data.RequiresEmailConfirmation
};
}
}
}

@ -14,22 +14,30 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Data;
using Microsoft.Extensions.FileProviders;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
public class HomeController : Controller
{
private readonly CssThemeManager _cachedServerSettings;
private readonly IFileProvider _fileProvider;
public IHttpClientFactory HttpClientFactory { get; }
SignInManager<ApplicationUser> SignInManager { get; }
public HomeController(IHttpClientFactory httpClientFactory,
CssThemeManager cachedServerSettings,
IWebHostEnvironment webHostEnvironment,
SignInManager<ApplicationUser> signInManager)
{
HttpClientFactory = httpClientFactory;
_cachedServerSettings = cachedServerSettings;
_fileProvider = webHostEnvironment.WebRootFileProvider;
SignInManager = signInManager;
}
@ -105,6 +113,30 @@ namespace BTCPayServer.Controllers
return View(new BitpayTranslatorViewModel());
}
[Route("swagger/v1/swagger.json")]
public async Task<IActionResult> Swagger()
{
JObject json = new JObject();
var directoryContents = _fileProvider.GetDirectoryContents("swagger/v1");
foreach (IFileInfo fi in directoryContents)
{
await using var stream = fi.CreateReadStream();
using var reader = new StreamReader(fi.CreateReadStream());
json.Merge(JObject.Parse(await reader.ReadToEndAsync()));
}
var servers = new JArray();
servers.Add(new JObject(new JProperty("url", HttpContext.Request.GetAbsoluteRoot())));
json["servers"] = servers;
return Json(json);
}
[Route("docs")]
public IActionResult SwaggerDocs()
{
return View();
}
[HttpPost]
[Route("translate")]
public async Task<IActionResult> BitpayTranslator(BitpayTranslatorViewModel vm)

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Security;
@ -12,7 +13,7 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Bitpay)]
[Authorize(Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Bitpay)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;

@ -6,6 +6,7 @@ using System.Net.Mime;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
@ -230,9 +231,9 @@ namespace BTCPayServer.Controllers
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en",
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS,
CustomLogoLink = storeBlob.CustomLogo,
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ToString(),
@ -510,7 +511,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices/create")]
[Authorize(Policy = Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{

@ -154,6 +154,7 @@ namespace BTCPayServer.Controllers
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
@ -179,7 +180,8 @@ namespace BTCPayServer.Controllers
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
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/getting-started/connectwallet)");
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
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/getting-started/connectwallet)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());
@ -257,7 +259,7 @@ namespace BTCPayServer.Controllers
using (logs.Measure($"{logPrefix} Payment method details creation"))
{
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
var paymentDetails = await handler.CreatePaymentMethodDetails(logs, supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
}

@ -162,23 +162,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodesWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' because they do not have 2FA enabled.");
}
return View(nameof(GenerateRecoveryCodesWarning));
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(CultureInfo.InvariantCulture,

@ -3,14 +3,16 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Hosting.OpenApi;
using BTCPayServer.Models;
using BTCPayServer.Security;
using BTCPayServer.Security.APIKeys;
using BTCPayServer.Security.GreenField;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using NBitcoin;
using NBitcoin.DataEncoders;
using YamlDotNet.Core.Tokens;
namespace BTCPayServer.Controllers
{
@ -23,13 +25,11 @@ namespace BTCPayServer.Controllers
{
ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery()
{
UserId = new[] {_userManager.GetUserId(User)}
UserId = new[] { _userManager.GetUserId(User) }
})
});
}
[HttpGet("api-keys/{id}/delete")]
public async Task<IActionResult> RemoveAPIKey(string id)
{
@ -40,10 +40,10 @@ namespace BTCPayServer.Controllers
}
return View("Confirm", new ConfirmModel()
{
Title = "Delete API Key "+ ( string.IsNullOrEmpty(key.Label)? string.Empty: key.Label) + "("+key.Id+")",
Title = "Delete API Key " + (string.IsNullOrEmpty(key.Label) ? string.Empty : key.Label) + "(" + key.Id + ")",
Description = "Any application using this api key will immediately lose access",
Action = "Delete",
ActionUrl = Request.GetCurrentUrl().Replace("RemoveAPIKey", "RemoveAPIKeyPost")
ActionUrl = this.Url.ActionLink(nameof(RemoveAPIKeyPost), values: new { id = id })
});
}
@ -80,15 +80,7 @@ namespace BTCPayServer.Controllers
return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel()));
}
/// <param name="permissions">The permissions to request. Current permissions available: ServerManagement, StoreManagement</param>
/// <param name="applicationName">The name of your application</param>
/// <param name="strict">If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting.</param>
/// <param name="selectiveStores">If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control.</param>
[HttpGet("~/api-keys/authorize")]
[OpenApiTags("Authorization")]
[OpenApiOperation("Authorize User",
"Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions")]
[IncludeInOpenApiDocs]
public async Task<IActionResult> AuthorizeAPIKey(string[] permissions, string applicationName = null,
bool strict = true, bool selectiveStores = false)
{
@ -101,72 +93,93 @@ namespace BTCPayServer.Controllers
});
return RedirectToAction("APIKeys");
}
permissions ??= Array.Empty<string>();
var parsedPermissions = Permission.ToPermissions(permissions).GroupBy(permission => permission.Policy);
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel()
{
Label = applicationName,
ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement),
StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement),
PermissionsFormatted = permissions,
ApplicationName = applicationName,
SelectiveStores = selectiveStores,
Strict = strict,
Permissions = string.Join(';', parsedPermissions.SelectMany(grouping => grouping.Select(permission => permission.ToString())))
});
vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin;
AdjustVMForAuthorization(vm);
return View(vm);
}
private void AdjustVMForAuthorization(AuthorizeApiKeysViewModel vm)
{
var parsedPermissions = Permission.ToPermissions(vm.Permissions.Split(';')).GroupBy(permission => permission.Policy);
for (var index = vm.PermissionValues.Count - 1; index >= 0; index--)
{
var permissionValue = vm.PermissionValues[index];
var wanted = parsedPermissions?.SingleOrDefault(permission =>
permission.Key.Equals(permissionValue.Permission,
StringComparison.InvariantCultureIgnoreCase));
if (vm.Strict && !(wanted?.Any()??false))
{
vm.PermissionValues.RemoveAt(index);
continue;
}
else if (wanted?.Any()??false)
{
if (vm.SelectiveStores && Policies.IsStorePolicy(permissionValue.Permission) &&
wanted.Any(permission => !string.IsNullOrEmpty(permission.StoreId)))
{
permissionValue.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.Specific;
permissionValue.SpecificStores = wanted.Select(permission => permission.StoreId).ToList();
}
else
{
permissionValue.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores;
permissionValue.SpecificStores = new List<string>();
permissionValue.Value = true;
}
}
}
}
[HttpPost("~/api-keys/authorize")]
public async Task<IActionResult> AuthorizeAPIKey([FromForm] AuthorizeApiKeysViewModel viewModel)
{
await SetViewModelValues(viewModel);
AdjustVMForAuthorization(viewModel);
var ar = HandleCommands(viewModel);
if (ar != null)
{
return ar;
}
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement))
for (int i = 0; i < viewModel.PermissionValues.Count; i++)
{
if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
if (viewModel.PermissionValues[i].Forbidden && viewModel.Strict)
{
viewModel.ServerManagementPermission = false;
viewModel.PermissionValues[i].Value = false;
ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value",
$"The permission '{viewModel.PermissionValues[i].Title}' is required for this application.");
}
if (!viewModel.ServerManagementPermission && viewModel.Strict)
if (viewModel.PermissionValues[i].StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
!viewModel.SelectiveStores)
{
ModelState.AddModelError(nameof(viewModel.ServerManagementPermission),
"This permission is required for this application.");
viewModel.PermissionValues[i].StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores;
ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value",
$"The permission '{viewModel.PermissionValues[i].Title}' cannot be store specific for this application.");
}
}
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement))
{
if (!viewModel.SelectiveStores &&
viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{
viewModel.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores;
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
"This application does not allow selective store permissions.");
}
if (!viewModel.StoreManagementPermission && !viewModel.SpecificStores.Any() && viewModel.Strict)
{
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
"This permission is required for this application.");
}
}
if (!ModelState.IsValid)
{
return View(viewModel);
}
switch (viewModel.Command.ToLowerInvariant())
{
case "no":
@ -176,10 +189,11 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"API key generated! <code>{key.Id}</code>"
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
});
return RedirectToAction("APIKeys", new { key = key.Id});
default: return View(viewModel);
return RedirectToAction("APIKeys", new { key = key.Id });
default:
return View(viewModel);
}
}
@ -205,41 +219,54 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"API key generated! <code>{key.Id}</code>"
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
});
return RedirectToAction("APIKeys");
}
private IActionResult HandleCommands(AddApiKeyViewModel viewModel)
{
switch (viewModel.Command)
if (string.IsNullOrEmpty(viewModel.Command))
{
return null;
}
var parts = viewModel.Command.Split(':', StringSplitOptions.RemoveEmptyEntries);
var permission = parts[0];
if (!Policies.IsStorePolicy(permission))
{
return null;
}
var permissionValueItem = viewModel.PermissionValues.Single(item => item.Permission == permission);
var command = parts[1];
var storeIndex = parts.Length == 3 ? parts[2] : null;
ModelState.Clear();
switch (command)
{
case "change-store-mode":
viewModel.StoreMode = viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
permissionValueItem.StoreMode = permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
? AddApiKeyViewModel.ApiKeyStoreMode.AllStores
: AddApiKeyViewModel.ApiKeyStoreMode.Specific;
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
!viewModel.SpecificStores.Any() && viewModel.Stores.Any())
if (permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
!permissionValueItem.SpecificStores.Any() && viewModel.Stores.Any())
{
viewModel.SpecificStores.Add(null);
permissionValueItem.SpecificStores.Add(null);
}
return View(viewModel);
case "add-store":
viewModel.SpecificStores.Add(null);
permissionValueItem.SpecificStores.Add(null);
return View(viewModel);
case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase):
case "remove-store":
{
ModelState.Clear();
var index = int.Parse(
viewModel.Command.Substring(
viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
CultureInfo.InvariantCulture);
viewModel.SpecificStores.RemoveAt(index);
if (storeIndex != null)
permissionValueItem.SpecificStores.RemoveAt(int.Parse(storeIndex,
CultureInfo.InvariantCulture));
return View(viewModel);
}
}
return null;
}
@ -247,41 +274,65 @@ namespace BTCPayServer.Controllers
{
var key = new APIKeyData()
{
Id = Guid.NewGuid().ToString().Replace("-", string.Empty),
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
Type = APIKeyType.Permanent,
UserId = _userManager.GetUserId(User),
Label = viewModel.Label
};
key.SetPermissions(GetPermissionsFromViewModel(viewModel));
key.SetBlob(new APIKeyBlob()
{
Permissions = GetPermissionsFromViewModel(viewModel).Select(p => p.ToString()).Distinct().ToArray()
});
await _apiKeyRepository.CreateKey(key);
return key;
}
private IEnumerable<string> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
private IEnumerable<Permission> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
{
var permissions = new List<string>();
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
List<Permission> permissions = new List<Permission>();
foreach (var p in viewModel.PermissionValues.Where(tuple => !tuple.Forbidden))
{
permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission));
}
else if (viewModel.StoreManagementPermission)
{
permissions.Add(APIKeyConstants.Permissions.StoreManagement);
if (Policies.IsStorePolicy(p.Permission))
{
if (p.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && p.Value)
{
permissions.Add(Permission.Create(p.Permission));
}
else if (p.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{
permissions.AddRange(p.SpecificStores.Select(s => Permission.Create(p.Permission, s)));
}
}
else if (p.Value && Permission.TryCreatePermission(p.Permission, null, out var pp))
permissions.Add(pp);
}
if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
{
permissions.Add(APIKeyConstants.Permissions.ServerManagement);
}
return permissions;
return permissions.Distinct();
}
private async Task<T> SetViewModelValues<T>(T viewModel) where T : AddApiKeyViewModel
{
viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User));
viewModel.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
viewModel.PermissionValues ??= Policies.AllPolicies
.Select(s => new AddApiKeyViewModel.PermissionValueItem()
{
Permission = s,
Value = false,
Forbidden = Policies.IsServerPolicy(s) && !isAdmin
}).ToList();
if (!isAdmin)
{
foreach (var p in viewModel.PermissionValues.Where(item => Policies.IsServerPolicy(item.Permission)))
{
p.Forbidden = true;
}
}
return viewModel;
}
@ -289,18 +340,52 @@ namespace BTCPayServer.Controllers
{
public string Label { get; set; }
public StoreData[] Stores { get; set; }
public ApiKeyStoreMode StoreMode { get; set; }
public List<string> SpecificStores { get; set; } = new List<string>();
public bool IsServerAdmin { get; set; }
public bool ServerManagementPermission { get; set; }
public bool StoreManagementPermission { get; set; }
public string Command { get; set; }
public List<PermissionValueItem> PermissionValues { get; set; }
public enum ApiKeyStoreMode
{
AllStores,
Specific
}
public class PermissionValueItem
{
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
{
{BTCPayServer.Client.Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")},
{BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")},
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
{$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
{BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")},
{BTCPayServer.Client.Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")},
{BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
{BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")},
{$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")},
};
public string Title
{
get
{
return PermissionDescriptions[$"{Permission}{(StoreMode == ApiKeyStoreMode.Specific? ":": "")}"].Title;
}
}
public string Description
{
get
{
return PermissionDescriptions[$"{Permission}{(StoreMode == ApiKeyStoreMode.Specific? ":": "")}"].Description;
}
}
public string Permission { get; set; }
public bool Value { get; set; }
public bool Forbidden { get; set; }
public ApiKeyStoreMode StoreMode { get; set; } = ApiKeyStoreMode.AllStores;
public List<string> SpecificStores { get; set; } = new List<string>();
}
}
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel
@ -309,18 +394,6 @@ namespace BTCPayServer.Controllers
public bool Strict { get; set; }
public bool SelectiveStores { get; set; }
public string Permissions { get; set; }
public string[] PermissionsFormatted
{
get
{
return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries)?? Array.Empty<string>();
}
set
{
Permissions = string.Join(';', value ?? Array.Empty<string>());
}
}
}

@ -19,8 +19,8 @@ using System.Globalization;
using BTCPayServer.Security;
using BTCPayServer.U2F;
using BTCPayServer.Data;
using BTCPayServer.Security.APIKeys;
using Microsoft.AspNetCore.Routing;
using BTCPayServer.Security.GreenField;
namespace BTCPayServer.Controllers
{
@ -38,6 +38,7 @@ namespace BTCPayServer.Controllers
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly APIKeyRepository _apiKeyRepository;
private readonly IAuthorizationService _authorizationService;
private readonly LinkGenerator _linkGenerator;
StoreRepository _StoreRepository;
@ -54,7 +55,8 @@ namespace BTCPayServer.Controllers
U2FService u2FService,
BTCPayServerEnvironment btcPayServerEnvironment,
APIKeyRepository apiKeyRepository,
IAuthorizationService authorizationService
IAuthorizationService authorizationService,
LinkGenerator linkGenerator
)
{
_userManager = userManager;
@ -67,6 +69,7 @@ namespace BTCPayServer.Controllers
_btcPayServerEnvironment = btcPayServerEnvironment;
_apiKeyRepository = apiKeyRepository;
_authorizationService = authorizationService;
_linkGenerator = linkGenerator;
_StoreRepository = storeRepository;
}
@ -146,7 +149,7 @@ namespace BTCPayServer.Controllers
{
if (!ModelState.IsValid)
{
return View(model);
return View(nameof(Index), model);
}
var user = await _userManager.GetUserAsync(User);
@ -156,7 +159,7 @@ namespace BTCPayServer.Controllers
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
var email = user.Email;
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email.";

@ -98,7 +98,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html = $"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
Html = $"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction("GetPaymentRequests");
@ -225,6 +225,10 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default)
{
if (amount.HasValue && amount.Value <= 0)
{
return BadRequest("Please provide an amount greater than 0");
}
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{

@ -20,7 +20,7 @@ using BTCPayServer.Security.Bitpay;
namespace BTCPayServer.Controllers
{
[EnableCors(CorsPolicies.All)]
[Authorize(Policy = Policies.CanGetRates.Key, AuthenticationSchemes = Security.AuthenticationSchemes.Bitpay)]
[Authorize(Policy = ServerPolicies.CanGetRates.Key, AuthenticationSchemes = Security.AuthenticationSchemes.Bitpay)]
public class RateController : Controller
{
public StoreData CurrentStore

@ -1,23 +0,0 @@
using BTCPayServer.Data;
namespace BTCPayServer.Controllers.RestApi.ApiKeys
{
public class ApiKeyData
{
public string ApiKey { get; set; }
public string Label { get; set; }
public string UserId { get; set; }
public string[] Permissions { get; set; }
public static ApiKeyData FromModel(APIKeyData data)
{
return new ApiKeyData()
{
Permissions = data.GetPermissions(),
ApiKey = data.Id,
UserId = data.UserId,
Label = data.Label
};
}
}
}

@ -1,37 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Hosting.OpenApi;
using BTCPayServer.Security;
using BTCPayServer.Security.APIKeys;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
namespace BTCPayServer.Controllers.RestApi.ApiKeys
{
[ApiController]
[IncludeInOpenApiDocs]
[OpenApiTags("API Keys")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public class ApiKeysController : ControllerBase
{
private readonly APIKeyRepository _apiKeyRepository;
public ApiKeysController(APIKeyRepository apiKeyRepository)
{
_apiKeyRepository = apiKeyRepository;
}
[OpenApiOperation("Get current API Key information", "View information about the current API key")]
[SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData),
Description = "Information about the current api key")]
[HttpGet("~/api/v1/api-keys/current")]
[HttpGet("~/api/v1/users/me/api-keys/current")]
public async Task<ActionResult<ApiKeyData>> GetKey()
{
ControllerContext.HttpContext.GetAPIKey(out var apiKey);
var data = await _apiKeyRepository.GetKey(apiKey);
return Ok(ApiKeyData.FromModel(data));
}
}
}

@ -35,10 +35,11 @@ using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key,
[Authorize(Policy = BTCPayServer.Client.Policies.CanModifyServerSettings,
AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes.Cookie)]
public partial class ServerController : Controller
{
@ -347,6 +348,14 @@ namespace BTCPayServer.Controllers
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var files = await _StoredFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
{
UserIds = new[] {userId},
});
await Task.WhenAll(files.Select(file => _FileService.RemoveFile(file.Id, userId)));
await _UserManager.DeleteAsync(user);
await _StoreRepository.CleanUnreachableStores();
TempData[WellKnownTempData.SuccessMessage] = "User deleted";

@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -22,6 +23,9 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers
{
@ -51,7 +55,9 @@ namespace BTCPayServer.Controllers
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.CanUseHotWallet = await CanUseHotWallet();
var hotWallet = await CanUseHotWallet();
vm.CanUseHotWallet = hotWallet.HotWallet;
vm.CanUseRPCImport = hotWallet.RPCImport;
return View(vm);
}
@ -328,11 +334,15 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
GenerateWalletRequest request)
{
if (!await CanUseHotWallet())
Logs.Events.LogInformation($"GenerateNBXWallet called {storeId}, {cryptoCode}");
var hotWallet = await CanUseHotWallet();
if (!hotWallet.HotWallet || (!hotWallet.RPCImport && request.ImportKeysToRPC))
{
return NotFound();
}
Logs.Events.LogInformation($"GenerateNBXWallet after CanUseHotWallet");
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
var response = await client.GenerateWalletAsync(request);
@ -343,13 +353,16 @@ namespace BTCPayServer.Controllers
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "There was an error generating your wallet. Is your node available?"
});
return RedirectToAction("AddDerivationScheme", new {storeId, cryptoCode});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
Logs.Events.LogInformation($"GenerateNBXWallet after GenerateWalletAsync");
var store = HttpContext.GetStoreData();
var result = await AddDerivationScheme(storeId,
new DerivationSchemeViewModel()
{
Confirmation = false,
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
Network = network,
RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = network.GetRootKeyPath(),
@ -362,24 +375,37 @@ namespace BTCPayServer.Controllers
Enabled = !store.GetStoreBlob()
.IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike))
}, cryptoCode);
TempData.SetStatusMessageModel(new StatusMessageModel()
if (!ModelState.IsValid || !(result is RedirectToActionResult))
return result;
TempData.Clear();
if (string.IsNullOrEmpty(request.ExistingMnemonic))
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = !string.IsNullOrEmpty(request.ExistingMnemonic)
? "Your wallet has been imported."
: $"Your wallet has been generated. Please store your seed securely! <br/><code>{response.Mnemonic}</code>"
});
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"Your wallet has been generated. Please store your seed securely! <br/><code class=\"alert-link\">{response.Mnemonic}</code>"
});
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = "Please check your addresses and confirm"
});
}
Logs.Events.LogInformation($"GenerateNBXWallet returning success result");
return result;
}
private async Task<bool> CanUseHotWallet()
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, BTCPayServer.Security.Policies.CanModifyServerSettings.Key)).Succeeded;
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
if (isAdmin)
return true;
return (true, true);
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return policies?.AllowHotWalletForAll is true;
var hotWallet = policies?.AllowHotWalletForAll is true;
return (hotWallet, hotWallet && policies?.AllowHotWalletRPCImportForAll is true);
}
private async Task<string> ReadAllText(IFormFile file)

@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
@ -17,6 +18,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -33,7 +35,7 @@ namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
@ -59,7 +61,8 @@ namespace BTCPayServer.Controllers
SettingsRepository settingsRepository,
IAuthorizationService authorizationService,
EventAggregator eventAggregator,
CssThemeManager cssThemeManager)
CssThemeManager cssThemeManager,
AppService appService)
{
_RateFactory = rateFactory;
_Repo = repo;
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
_settingsRepository = settingsRepository;
_authorizationService = authorizationService;
_CssThemeManager = cssThemeManager;
_appService = appService;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
@ -102,6 +106,7 @@ namespace BTCPayServer.Controllers
private readonly SettingsRepository _settingsRepository;
private readonly IAuthorizationService _authorizationService;
private readonly CssThemeManager _CssThemeManager;
private readonly AppService _appService;
private readonly EventAggregator _EventAggregator;
[TempData]
@ -474,6 +479,7 @@ namespace BTCPayServer.Controllers
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
return View(vm);
}
@ -568,7 +574,7 @@ namespace BTCPayServer.Controllers
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
blob.PaymentTolerance = model.PaymentTolerance;
blob.PayJoinEnabled = model.PayJoinEnabled;
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;
@ -888,7 +894,7 @@ namespace BTCPayServer.Controllers
const string DEFAULT_CURRENCY = "USD";
[Route("{storeId}/paybutton")]
public IActionResult PayButton()
public async Task<IActionResult> PayButton()
{
var store = CurrentStore;
@ -898,6 +904,7 @@ namespace BTCPayServer.Controllers
return View("PayButtonEnable", null);
}
var apps = await _appService.GetAllApps(_UserManager.GetUserId(User), false, store.Id);
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
@ -910,7 +917,8 @@ namespace BTCPayServer.Controllers
ButtonType = 0,
Min = 1,
Max = 20,
Step = 1
Step = 1,
Apps = apps
};
return View(model);
}

@ -7,6 +7,7 @@ using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Hwi;
using BTCPayServer.ModelBinders;
@ -127,7 +128,7 @@ namespace BTCPayServer.Controllers
}
await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings.Key);
var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings);
if (!authorization.Succeeded)
{
await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken);

@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
@ -20,6 +24,10 @@ namespace BTCPayServer.Controllers
{
var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
if (sendModel.InputSelection)
{
psbtRequest.IncludeOnlyOutpoints = sendModel.SelectedInputs?.Select(OutPoint.Parse)?.ToList()?? new List<OutPoint>();
}
foreach (var transactionOutput in sendModel.Outputs)
{
var psbtDestination = new CreatePSBTDestination();
@ -64,6 +72,7 @@ namespace BTCPayServer.Controllers
vm.Decoded = psbt.ToString();
vm.PSBT = psbt.ToBase64();
}
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
}
[HttpPost]
@ -94,12 +103,12 @@ namespace BTCPayServer.Controllers
vm.FileName = vm.UploadedPSBTFile?.FileName;
return View(vm);
case "vault":
return ViewVault(walletId, psbt);
return ViewVault(walletId, psbt, vm.PayJoinEndpointUrl);
case "ledger":
return ViewWalletSendLedger(walletId, psbt);
case "update":
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
psbt = await UpdatePSBT(derivationSchemeSettings, psbt, network);
psbt = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbt);
if (psbt == null)
{
ModelState.AddModelError(nameof(vm.PSBT), "You need to update your version of NBXplorer");
@ -108,7 +117,7 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
return RedirectToWalletPSBT(psbt, vm.FileName);
case "seed":
return SignWithSeed(walletId, psbt.ToBase64());
return SignWithSeed(walletId, psbt.ToBase64(), vm.PayJoinEndpointUrl);
case "nbx-seed":
if (await CanUseHotWallet())
{
@ -118,7 +127,7 @@ namespace BTCPayServer.Controllers
WellknownMetadataKeys.MasterHDKey);
return SignWithSeed(walletId,
new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()});
new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64(), PayJoinEndpointUrl = vm.PayJoinEndpointUrl});
}
return View(vm);
@ -136,31 +145,32 @@ namespace BTCPayServer.Controllers
}
}
private async Task<PSBT> UpdatePSBT(DerivationSchemeSettings derivationSchemeSettings, PSBT psbt, BTCPayNetwork network)
private async Task<PSBT> GetPayjoinProposedTX(string bpu, PSBT psbt, DerivationSchemeSettings derivationSchemeSettings, BTCPayNetwork btcPayNetwork, CancellationToken cancellationToken)
{
var result = await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(new UpdatePSBTRequest()
{
PSBT = psbt,
DerivationScheme = derivationSchemeSettings.AccountDerivation,
});
if (result == null)
return null;
derivationSchemeSettings.RebaseKeyPaths(result.PSBT);
return result.PSBT;
if (string.IsNullOrEmpty(bpu) || !Uri.TryCreate(bpu, UriKind.Absolute, out var endpoint))
throw new InvalidOperationException("No payjoin url available");
var cloned = psbt.Clone();
cloned = cloned.Finalize();
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), cloned.ExtractTransaction(), btcPayNetwork);
return await _payjoinClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, cancellationToken);
}
[HttpGet]
[Route("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string psbt = null,
string signingKey = null,
string signingKeyPath = null)
string signingKeyPath = null,
string originalPsbt = null,
string payJoinEndpointUrl = null)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var vm = new WalletPSBTReadyViewModel() { PSBT = psbt };
vm.SigningKey = signingKey;
vm.SigningKeyPath = signingKeyPath;
vm.OriginalPSBT = originalPsbt;
vm.PayJoinEndpointUrl = payJoinEndpointUrl;
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
return NotFound();
@ -176,7 +186,7 @@ namespace BTCPayServer.Controllers
{
var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
if (!psbtObject.IsAllFinalized())
psbtObject = await UpdatePSBT(derivationSchemeSettings, psbtObject, network) ?? psbtObject;
psbtObject = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbtObject) ?? psbtObject;
IHDKey signingKey = null;
RootedKeyPath signingKeyPath = null;
try
@ -278,16 +288,17 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
{
if (command == null)
return await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath);
return await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath, vm.OriginalPSBT, vm.PayJoinEndpointUrl);
PSBT psbt = null;
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
DerivationSchemeSettings derivationSchemeSettings = null;
try
{
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
return NotFound();
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
@ -297,38 +308,115 @@ namespace BTCPayServer.Controllers
vm.GlobalError = "Invalid PSBT";
return View(nameof(WalletPSBTReady),vm);
}
if (command == "broadcast")
switch (command)
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
case "payjoin":
string error = null;
try
{
var proposedPayjoin = await GetPayjoinProposedTX(vm.PayJoinEndpointUrl, psbt,
derivationSchemeSettings, network, cancellationToken);
try
{
var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
extKey,
RootedKeyPath.Parse(vm.SigningKeyPath));
vm.PSBT = proposedPayjoin.ToBase64();
vm.OriginalPSBT = psbt.ToBase64();
proposedPayjoin.Finalize();
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
AllowDismiss = false,
Html = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
});
return await WalletPSBTReady(walletId, vm, "broadcast");
}
catch (Exception)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
AllowDismiss = false,
Html =
$"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" +
$"The amount being sent may appear higher but is in fact almost same.<br/><br/>" +
$"If you cancel refuse to sign this transaction, the payment will proceed without payjoin"
});
return ViewVault(walletId, proposedPayjoin, vm.PayJoinEndpointUrl, psbt);
}
}
catch (PayjoinReceiverException ex)
{
error = $"The payjoin receiver could not complete the payjoin: {ex.Message}";
}
catch (PayjoinSenderException ex)
{
error = $"We rejected the receiver's payjoin proposal: {ex.Message}";
}
catch (Exception ex)
{
error = $"Unexpected payjoin error: {ex.Message}";
}
//we possibly exposed the tx to the receiver, so we need to broadcast straight away
psbt.Finalize();
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
AllowDismiss = false,
Html = $"The payjoin transaction could not be created.<br/>" +
$"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" +
$"{error}"
});
return await WalletPSBTReady(walletId, vm, "broadcast");
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
vm.SetErrors(errors);
return View(nameof(WalletPSBTReady),vm);
}
var transaction = psbt.ExtractTransaction();
try
case "broadcast":
{
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
var transaction = psbt.ExtractTransaction();
try
{
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
{
if (!string.IsNullOrEmpty(vm.OriginalPSBT))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
AllowDismiss = false,
Html = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast."
});
vm.PSBT = vm.OriginalPSBT;
vm.OriginalPSBT = null;
return await WalletPSBTReady(walletId, vm, "broadcast");
}
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
return View(nameof(WalletPSBTReady),vm);
}
}
catch (Exception ex)
{
vm.GlobalError = "Error while broadcasting: " + ex.Message;
return View(nameof(WalletPSBTReady),vm);
}
if (!TempData.HasStatusMessage())
{
TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
}
return RedirectToWalletTransaction(walletId, transaction);
}
catch (Exception ex)
{
vm.GlobalError = "Error while broadcasting: " + ex.Message;
case "analyze-psbt":
return RedirectToWalletPSBT(psbt);
default:
vm.GlobalError = "Unknown command";
return View(nameof(WalletPSBTReady),vm);
}
return RedirectToWalletTransaction(walletId, transaction);
}
else if (command == "analyze-psbt")
{
return RedirectToWalletPSBT(psbt);
}
else
{
vm.GlobalError = "Unknown command";
return View(nameof(WalletPSBTReady),vm);
}
}

@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
@ -30,7 +32,7 @@ using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public partial class WalletsController : Controller
{
@ -48,6 +50,8 @@ namespace BTCPayServer.Controllers
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly EventAggregator _EventAggregator;
private readonly SettingsRepository _settingsRepository;
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly PayjoinClient _payjoinClient;
public RateFetcher RateFetcher { get; }
CurrencyNameTable _currencyTable;
@ -65,7 +69,9 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider,
WalletReceiveStateService walletReceiveStateService,
EventAggregator eventAggregator,
SettingsRepository settingsRepository)
SettingsRepository settingsRepository,
DelayedTransactionBroadcaster broadcaster,
PayjoinClient payjoinClient)
{
_currencyTable = currencyTable;
Repository = repo;
@ -82,6 +88,8 @@ namespace BTCPayServer.Controllers
_WalletReceiveStateService = walletReceiveStateService;
_EventAggregator = eventAggregator;
_settingsRepository = settingsRepository;
_broadcaster = broadcaster;
_payjoinClient = payjoinClient;
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
@ -366,7 +374,7 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
@ -455,7 +463,9 @@ namespace BTCPayServer.Controllers
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey, cancellation));
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(vm, bip21, network);
@ -463,7 +473,36 @@ namespace BTCPayServer.Controllers
}
decimal transactionAmountSum = 0;
if (command == "toggle-input-selection")
{
vm.InputSelection = !vm.InputSelection;
}
if (vm.InputSelection)
{
var schemeSettings = GetDerivationSchemeSettings(walletId);
var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await _walletProvider.GetWallet(network).GetUnspentCoins(schemeSettings.AccountDerivation, cancellation);
vm.InputsAvailable = utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
return new WalletSendModel.InputSelectionOption()
{
Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = info == null? null :walletBlobAsync.GetLabels(info),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString())
};
}).ToArray();
}
if (command == "toggle-input-selection")
{
ModelState.Clear();
return View(vm);
}
if (command == "add-output")
{
ModelState.Clear();
@ -564,28 +603,28 @@ namespace BTCPayServer.Controllers
return View(vm);
}
derivationScheme.RebaseKeyPaths(psbt.PSBT);
switch (command)
{
case "vault":
return ViewVault(walletId, psbt.PSBT);
return ViewVault(walletId, psbt.PSBT, vm.PayJoinEndpointUrl);
case "nbx-seed":
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation);
return SignWithSeed(walletId, new SignWithSeedViewModel()
{
PayJoinEndpointUrl = vm.PayJoinEndpointUrl,
SeedOrKey = extKey,
PSBT = psbt.PSBT.ToBase64()
});
case "ledger":
return ViewWalletSendLedger(walletId, psbt.PSBT, psbt.ChangeAddress);
case "seed":
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
return SignWithSeed(walletId, psbt.PSBT.ToBase64(), vm.PayJoinEndpointUrl);
case "analyze-psbt":
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
return RedirectToWalletPSBT(psbt.PSBT, name);
return RedirectToWalletPSBT(psbt.PSBT, name, vm.PayJoinEndpointUrl);
default:
return View(vm);
}
@ -620,24 +659,41 @@ namespace BTCPayServer.Controllers
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
});
}
uriBuilder.UnknowParameters.TryGetValue(PayjoinClient.BIP21EndpointKey, out var vmPayJoinEndpointUrl);
vm.PayJoinEndpointUrl = vmPayJoinEndpointUrl;
}
catch (Exception)
catch
{
TempData.SetStatusMessageModel(new StatusMessageModel()
try
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The provided BIP21 payment URI was malformed"
});
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
}
};
}
catch
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The provided BIP21 payment URI was malformed"
});
}
}
ModelState.Clear();
}
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
private IActionResult ViewVault(WalletId walletId, PSBT psbt, string payJoinEndpointUrl, PSBT originalPSBT = null)
{
return View("WalletSendVault", new WalletSendVaultModel()
return View(nameof(WalletSendVault), new WalletSendVaultModel()
{
PayJoinEndpointUrl = payJoinEndpointUrl,
WalletId = walletId.ToString(),
OriginalPSBT = originalPSBT?.ToBase64(),
PSBT = psbt.ToBase64(),
WebsocketPath = this.Url.Action(nameof(VaultController.VaultBridgeConnection), "Vault", new { walletId = walletId.ToString() })
});
@ -645,12 +701,12 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{walletId}/vault")]
public IActionResult SubmitVault([ModelBinder(typeof(WalletIdModelBinder))]
public IActionResult WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendVaultModel model)
{
return RedirectToWalletPSBTReady(model.PSBT);
return RedirectToWalletPSBTReady(model.PSBT, originalPsbt: model.OriginalPSBT, payJoinEndpointUrl: model.PayJoinEndpointUrl);
}
private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null)
private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null, string originalPsbt = null, string payJoinEndpointUrl = null)
{
var vm = new PostRedirectViewModel()
{
@ -659,6 +715,8 @@ namespace BTCPayServer.Controllers
Parameters =
{
new KeyValuePair<string, string>("psbt", psbt),
new KeyValuePair<string, string>("originalPsbt", originalPsbt),
new KeyValuePair<string, string>("payJoinEndpointUrl", payJoinEndpointUrl),
new KeyValuePair<string, string>("SigningKey", signingKey),
new KeyValuePair<string, string>("SigningKeyPath", signingKeyPath)
}
@ -666,7 +724,7 @@ namespace BTCPayServer.Controllers
return View("PostRedirect", vm);
}
private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null)
private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null, string payJoinEndpointUrl = null)
{
var vm = new PostRedirectViewModel()
{
@ -679,6 +737,8 @@ namespace BTCPayServer.Controllers
};
if (!string.IsNullOrEmpty(fileName))
vm.Parameters.Add(new KeyValuePair<string, string>("fileName", fileName));
if (!string.IsNullOrEmpty(payJoinEndpointUrl))
vm.Parameters.Add(new KeyValuePair<string, string>("payJoinEndpointUrl", payJoinEndpointUrl));
return View("PostRedirect", vm);
}
@ -725,10 +785,11 @@ namespace BTCPayServer.Controllers
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,string psbt)
WalletId walletId,string psbt, string payJoinEndpointUrl)
{
return View(nameof(SignWithSeed), new SignWithSeedViewModel()
{
PayJoinEndpointUrl = payJoinEndpointUrl,
PSBT = psbt
});
}
@ -796,9 +857,10 @@ namespace BTCPayServer.Controllers
return View(viewModel);
}
ModelState.Remove(nameof(viewModel.PSBT));
return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString(), viewModel.OriginalPSBT, viewModel.PayJoinEndpointUrl);
}
private bool PSBTChanged(PSBT psbt, Action act)
{
var before = psbt.ToBase64();
@ -820,7 +882,6 @@ namespace BTCPayServer.Controllers
var wallet = _walletProvider.GetWallet(network);
var derivationSettings = GetDerivationSchemeSettings(walletId);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})";
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
}
@ -839,7 +900,7 @@ namespace BTCPayServer.Controllers
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
@ -869,7 +930,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel vm)

@ -0,0 +1,26 @@
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public static class APIKeyDataExtensions
{
public static APIKeyBlob GetBlob(this APIKeyData apiKeyData)
{
var result = apiKeyData.Blob == null
? new APIKeyBlob()
: JObject.Parse(ZipUtils.Unzip(apiKeyData.Blob)).ToObject<APIKeyBlob>();
return result;
}
public static bool SetBlob(this APIKeyData apiKeyData, APIKeyBlob blob)
{
var original = new Serializer(null).ToString(apiKeyData.GetBlob());
var newBlob = new Serializer(null).ToString(blob);
if (original == newBlob)
return false;
apiKeyData.Blob = ZipUtils.Zip(newBlob);
return true;
}
}
}

@ -93,9 +93,8 @@ namespace BTCPayServer.Data
public CurrencyValue LightningMaxValue { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public string CustomLogo { get; set; }
public string CustomCSS { get; set; }
public string CustomLogo { get; set; }
public string HtmlTitle { get; set; }
public bool RateScripting { get; set; }
@ -173,6 +172,7 @@ namespace BTCPayServer.Data
public EmailSettings EmailSettings { get; set; }
public bool RedirectAutomatically { get; set; }
public bool PayJoinEnabled { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{

@ -67,6 +67,8 @@ namespace BTCPayServer.Data
public static IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(this StoreData storeData, BTCPayNetworkProvider networks)
{
if (storeData == null)
throw new ArgumentNullException(nameof(storeData));
networks = networks.UnfilteredNetworks;
#pragma warning disable CS0618
bool btcReturned = false;

@ -6,5 +6,11 @@ namespace BTCPayServer.Events
{
public NewTransactionEvent NewTransactionEvent { get; set; }
public string CryptoCode { get; set; }
public override string ToString()
{
var state = NewTransactionEvent.BlockId == null ? "Unconfirmed" : "Confirmed";
return $"{CryptoCode}: New transaction {NewTransactionEvent.TransactionData.TransactionHash} ({state})";
}
}
}

@ -0,0 +1,13 @@
using System;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Events
{
public class UserRegisteredEvent
{
public ApplicationUser User { get; set; }
public bool Admin { get; set; }
public Uri RequestUri { get; set; }
}
}

@ -1,7 +1,13 @@
using System;
namespace BTCPayServer.Events
{
public class WalletChangedEvent
{
public WalletId WalletId { get; set; }
public override string ToString()
{
return String.Empty;
}
}
}

@ -36,6 +36,7 @@ using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json.Linq;
using BTCPayServer.Payments.Bitcoin;
namespace BTCPayServer
{
@ -136,15 +137,37 @@ namespace BTCPayServer
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice)
{
return invoice.GetPayments()
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, bool includeOffchain = false, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
var transactions = hashes
.Select(async o => await client.GetTransactionAsync(o, cts))
.Select(async o => await client.GetTransactionAsync(o, includeOffchain, cts))
.ToArray();
await Task.WhenAll(transactions).ConfigureAwait(false);
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
}
public static async Task<PSBT> UpdatePSBT(this ExplorerClientProvider explorerClientProvider, DerivationSchemeSettings derivationSchemeSettings, PSBT psbt)
{
var result = await explorerClientProvider.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode).UpdatePSBTAsync(new UpdatePSBTRequest()
{
PSBT = psbt,
DerivationScheme = derivationSchemeSettings.AccountDerivation
});
if (result == null)
return null;
derivationSchemeSettings.RebaseKeyPaths(result.PSBT);
return result.PSBT;
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/", StringComparison.InvariantCulture))
@ -425,6 +448,15 @@ namespace BTCPayServer
ctx.Items["BTCPAY.STOREDATA"] = storeData;
}
public static StoreData[] GetStoresData(this HttpContext ctx)
{
return ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[];
}
public static void SetStoresData(this HttpContext ctx, StoreData[] storeData)
{
ctx.Items["BTCPAY.STORESDATA"] = storeData;
}
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o)
{

@ -1,20 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using BTCPayServer.Controllers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc
{
public static class UrlHelperExtensions
{
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
public static string EmailConfirmationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
{
return urlHelper.Action(
action: nameof(AccountController.ConfirmEmail),
controller: "Account",
values: new { userId, code },
protocol: scheme);
return urlHelper.GetUriByAction( nameof(AccountController.ConfirmEmail), "Account",
new {userId, code}, scheme, host, pathbase);
}
public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme)

@ -22,7 +22,7 @@ namespace BTCPayServer.HostedServices
public void Update(ThemeSettings data)
{
if (String.IsNullOrWhiteSpace(data.ThemeCssUri))
_themeUri = "/main/themes/classic.css";
_themeUri = "/main/themes/default.css";
else
_themeUri = data.ThemeCssUri;

@ -0,0 +1,40 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class DelayedTransactionBroadcasterHostedService : BaseAsyncService
{
private readonly DelayedTransactionBroadcaster _transactionBroadcaster;
public DelayedTransactionBroadcasterHostedService(DelayedTransactionBroadcaster transactionBroadcaster)
{
_transactionBroadcaster = transactionBroadcaster;
}
internal override Task[] InitializeTasks()
{
return new Task[]
{
CreateLoopTask(Rebroadcast)
};
}
public TimeSpan PollInternal { get; set; } = TimeSpan.FromMinutes(1.0);
async Task Rebroadcast()
{
while (true)
{
await _transactionBroadcaster.ProcessAll(Cancellation);
await Task.Delay(PollInternal, Cancellation);
}
}
}
}

@ -38,14 +38,13 @@ namespace BTCPayServer.HostedServices
public bool IsFullySynched()
{
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
return _Summaries.All(s => s.Value.Status?.IsFullySynched is true);
}
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
summary.Status?.IsFullySynched is true;
}
public NBXplorerSummary Get(string cryptoCode)
{
@ -88,6 +87,7 @@ namespace BTCPayServer.HostedServices
_Client = client;
_Aggregator = aggregator;
_Dashboard = dashboard;
_Dashboard.Publish(_Network, State, null, null);
}
NBXplorerDashboard _Dashboard;

@ -0,0 +1,366 @@
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Lightning.Eclair.Models;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Socks;
namespace BTCPayServer.HostedServices
{
/// <summary>
/// This is a very simple Socks HTTP proxy, that can be used through HttpClient.WebProxy
/// However, it only supports a single request/response, so the client must specify Connection: close to not
/// reuse the TCP connection to the proxy for another requests.
/// Inspired from https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/
/// </summary>
public class Socks5HttpProxyServer : IHostedService
{
class ProxyConnection
{
public ServerContext ServerContext;
public Socket ClientSocket;
public Socket SocksSocket;
public CancellationToken CancellationToken;
public CancellationTokenSource CancellationTokenSource;
public void Dispose()
{
Socks5HttpProxyServer.Dispose(ClientSocket);
Socks5HttpProxyServer.Dispose(SocksSocket);
CancellationTokenSource.Dispose();
}
}
class ServerContext
{
public EndPoint SocksEndpoint;
public Socket ServerSocket;
public CancellationToken CancellationToken;
public int ConnectionCount;
}
private readonly BTCPayServerOptions _opts;
public Socks5HttpProxyServer(Configuration.BTCPayServerOptions opts)
{
_opts = opts;
}
private ServerContext _ServerContext;
private CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
if (_opts.SocksEndpoint is null || _ServerContext != null)
return Task.CompletedTask;
_Cts = new CancellationTokenSource();
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
Port = ((IPEndPoint)(socket.LocalEndPoint)).Port;
Uri = new Uri($"http://127.0.0.1:{Port}");
socket.Listen(5);
_ServerContext = new ServerContext()
{
SocksEndpoint = _opts.SocksEndpoint,
ServerSocket = socket,
CancellationToken = _Cts.Token,
ConnectionCount = 0
};
socket.BeginAccept(Accept, _ServerContext);
Logs.PayServer.LogInformation($"Internal Socks HTTP Proxy listening at {Uri}");
return Task.CompletedTask;
}
public int Port { get; private set; }
public Uri Uri { get; private set; }
static void Accept(IAsyncResult ar)
{
var ctx = (ServerContext)ar.AsyncState;
Socket clientSocket = null;
try
{
clientSocket = ctx.ServerSocket.EndAccept(ar);
}
catch (Exception)
{
return;
}
if (ctx.CancellationToken.IsCancellationRequested)
{
Dispose(clientSocket);
return;
}
var toSocksProxy = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var connectionCts = CancellationTokenSource.CreateLinkedTokenSource(ctx.CancellationToken);
toSocksProxy.BeginConnect(ctx.SocksEndpoint, ConnectToSocks, new ProxyConnection()
{
ServerContext = ctx,
ClientSocket = clientSocket,
SocksSocket = toSocksProxy,
CancellationToken = connectionCts.Token,
CancellationTokenSource = connectionCts
});
try
{
ctx.ServerSocket.BeginAccept(Accept, ctx);
}
catch (Exception)
{
return;
}
}
static void ConnectToSocks(IAsyncResult ar)
{
var connection = (ProxyConnection)ar.AsyncState;
try
{
connection.SocksSocket.EndConnect(ar);
}
catch (Exception)
{
connection.Dispose();
return;
}
Interlocked.Increment(ref connection.ServerContext.ConnectionCount);
var pipe = new Pipe(PipeOptions.Default);
var reading = FillPipeAsync(connection.ClientSocket, pipe.Writer, connection.CancellationToken)
.ContinueWith(_ => connection.CancellationTokenSource.Cancel(), TaskScheduler.Default);
var writing = ReadPipeAsync(connection.SocksSocket, connection.ClientSocket, pipe.Reader, connection.CancellationToken)
.ContinueWith(_ => connection.CancellationTokenSource.Cancel(), TaskScheduler.Default);
_ = Task.WhenAll(reading, writing)
.ContinueWith(_ =>
{
connection.Dispose();
Interlocked.Decrement(ref connection.ServerContext.ConnectionCount);
}, TaskScheduler.Default);
}
public int ConnectionCount => _ServerContext is ServerContext s ? s.ConnectionCount : 0;
private static async Task ReadPipeAsync(Socket socksSocket, Socket clientSocket, PipeReader reader, CancellationToken cancellationToken)
{
bool handshaked = false;
bool isConnect = false;
string firstHeader = null;
string httpVersion = null;
while (true)
{
ReadResult result = await reader.ReadAsync(cancellationToken);
ReadOnlySequence<byte> buffer = result.Buffer;
SequencePosition? position = null;
if (!handshaked)
{
nextchunk:
// Look for a EOL in the buffer
position = buffer.PositionOf((byte)'\n');
if (position == null)
goto readnext;
// Process the line
var line = GetHeaderLine(buffer.Slice(0, position.Value));
// Skip the line + the \n character (basically position)
buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
if (firstHeader is null)
{
firstHeader = line;
isConnect = line.StartsWith("CONNECT ", StringComparison.OrdinalIgnoreCase);
if (isConnect)
goto nextchunk;
else
goto handshake;
}
else if (line.Length == 1 && line[0] == '\r')
goto handshake;
else
goto nextchunk;
handshake:
var split = firstHeader.Split(' ');
if (split.Length != 3)
break;
var targetConnection = split[1].Trim();
EndPoint destinationEnpoint = null;
if (isConnect)
{
if (!Utils.TryParseEndpoint(targetConnection,
80,
out destinationEnpoint))
break;
}
else
{
if (!System.Uri.TryCreate(targetConnection, UriKind.Absolute, out var uri) ||
(uri.Scheme != "http" && uri.Scheme != "https"))
break;
if (!Utils.TryParseEndpoint($"{uri.DnsSafeHost}:{uri.Port}",
uri.Scheme == "http" ? 80 : 443,
out destinationEnpoint))
break;
firstHeader = $"{split[0]} {uri.PathAndQuery} {split[2].TrimEnd()}";
}
httpVersion = split[2].Trim();
try
{
await NBitcoin.Socks.SocksHelper.Handshake(socksSocket, destinationEnpoint, cancellationToken);
handshaked = true;
if (isConnect)
{
await SendAsync(clientSocket,
$"{httpVersion} 200 Connection established\r\nConnection: close\r\n\r\n",
cancellationToken);
}
else
{
await SendAsync(socksSocket, $"{firstHeader}\r\n", cancellationToken);
foreach (ReadOnlyMemory<byte> segment in buffer)
{
await socksSocket.SendAsync(segment, SocketFlags.None, cancellationToken);
}
buffer = buffer.Slice(buffer.End);
}
_ = Relay(socksSocket, clientSocket, cancellationToken);
}
catch (SocksException e) when (e.SocksErrorCode == SocksErrorCode.HostUnreachable || e.SocksErrorCode == SocksErrorCode.HostUnreachable)
{
await SendAsync(clientSocket , $"{httpVersion} 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n", cancellationToken);
goto done;
}
catch (SocksException e)
{
await SendAsync(clientSocket , $"{httpVersion} 500 Internal Server Error\r\nContent-Length: 0\r\nX-Proxy-Error-Type: Socks {e.SocksErrorCode}\r\n\r\n", cancellationToken);
goto done;
}
catch (SocketException e)
{
await SendAsync(clientSocket , $"{httpVersion} 500 Internal Server Error\r\nContent-Length: 0\r\nX-Proxy-Error-Type: Socket {e.SocketErrorCode}\r\n\r\n", cancellationToken);
goto done;
}
catch
{
await SendAsync(clientSocket , $"{httpVersion} 500 Internal Server Error\r\n\r\n", cancellationToken);
goto done;
}
}
else
{
foreach (ReadOnlyMemory<byte> segment in buffer)
{
await socksSocket.SendAsync(segment, SocketFlags.None, cancellationToken);
}
buffer = buffer.Slice(buffer.End);
}
readnext:
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.Start, buffer.End);
// Stop reading if there's no more data coming
if (result.IsCompleted)
{
break;
}
}
done:
// Mark the PipeReader as complete
reader.Complete();
}
private const int BufferSize = 1024 * 5;
private static async Task Relay(Socket from, Socket to, CancellationToken cancellationToken)
{
using var buffer = MemoryPool<byte>.Shared.Rent(BufferSize);
while (true)
{
int bytesRead = await from.ReceiveAsync(buffer.Memory, SocketFlags.None, cancellationToken);
if (bytesRead == 0)
break;
await to.SendAsync(buffer.Memory.Slice(0, bytesRead), SocketFlags.None, cancellationToken);
}
}
private static async Task SendAsync(Socket clientSocket, string data, CancellationToken cancellationToken)
{
var bytes = new byte[Encoding.ASCII.GetByteCount(data)];
Encoding.ASCII.GetBytes(data, bytes);
await clientSocket.SendAsync(bytes, SocketFlags.None, cancellationToken);
}
private static string GetHeaderLine(ReadOnlySequence<byte> buffer)
{
if (buffer.IsSingleSegment)
{
return Encoding.ASCII.GetString(buffer.First.Span);
}
return string.Create((int)buffer.Length, buffer, (span, sequence) =>
{
foreach (var segment in sequence)
{
Encoding.ASCII.GetChars(segment.Span, span);
span = span.Slice(segment.Length);
}
});
}
private static async Task FillPipeAsync(Socket socket, PipeWriter writer, CancellationToken cancellationToken)
{
while (true)
{
Memory<byte> memory = writer.GetMemory(BufferSize);
int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None, cancellationToken);
if (bytesRead == 0)
{
break;
}
writer.Advance(bytesRead);
FlushResult result = await writer.FlushAsync(cancellationToken);
if (result.IsCompleted)
{
break;
}
}
writer.Complete();
}
public Task StopAsync(CancellationToken cancellationToken)
{
if (_ServerContext is ServerContext ctx)
{
_Cts.Cancel();
Dispose(ctx.ServerSocket);
Logs.PayServer.LogInformation($"Internal Socks HTTP Proxy closed");
}
return Task.CompletedTask;
}
static void Dispose(Socket s)
{
try
{
s.Shutdown(SocketShutdown.Both);
s.Close();
}
catch (Exception)
{
}
finally
{
s.Dispose();
}
}
}
}

@ -0,0 +1,55 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.HostedServices
{
public class UserEventHostedService : EventHostedServiceBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly LinkGenerator _generator;
public UserEventHostedService(EventAggregator eventAggregator, UserManager<ApplicationUser> userManager,
EmailSenderFactory emailSenderFactory, LinkGenerator generator) : base(eventAggregator)
{
_userManager = userManager;
_emailSenderFactory = emailSenderFactory;
_generator = generator;
}
protected override void SubscibeToEvents()
{
Subscribe<UserRegisteredEvent>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
switch (evt)
{
case UserRegisteredEvent userRegisteredEvent:
Logs.PayServer.LogInformation($"A new user just registered {userRegisteredEvent.User.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
if (!userRegisteredEvent.User.EmailConfirmed && userRegisteredEvent.User.RequiresEmailConfirmation)
{
var code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
var callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code, userRegisteredEvent.RequestUri.Scheme, new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port), userRegisteredEvent.RequestUri.PathAndQuery);
_emailSenderFactory.GetEmailSender()
.SendEmailConfirmation(userRegisteredEvent.User.Email, callbackUrl);
}
break;
}
}
}
}

@ -26,14 +26,13 @@ using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting.OpenApi;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Security;
using BTCPayServer.Security.APIKeys;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
@ -45,6 +44,7 @@ using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security.Bitpay;
using Serilog;
using BTCPayServer.Security.GreenField;
namespace BTCPayServer.Hosting
{
@ -63,6 +63,7 @@ namespace BTCPayServer.Hosting
{
httpClient.Timeout = Timeout.InfiniteTimeSpan;
});
services.AddPayJoinServices();
services.AddMoneroLike();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<TorServices>();
@ -204,6 +205,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>();
services.AddSingleton<IHostedService, UserEventHostedService>();
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
@ -256,15 +258,17 @@ namespace BTCPayServer.Hosting
if (btcPayEnv.IsDevelopping)
{
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay");
rateLimits.SetZone($"zone={ZoneLimits.Register} rate=1000r/min burst=100 nodelay");
rateLimits.SetZone($"zone={ZoneLimits.PayJoin} rate=1000r/min burst=100 nodelay");
}
else
{
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
rateLimits.SetZone($"zone={ZoneLimits.Register} rate=2r/min burst=2 nodelay");
rateLimits.SetZone($"zone={ZoneLimits.PayJoin} rate=5r/min burst=3 nodelay");
}
return rateLimits;
});
services.AddBTCPayOpenApi();
services.AddLogging(logBuilder =>
{
var debugLogFile = BTCPayServerOptions.GetDebugLog(configuration);
@ -292,7 +296,6 @@ namespace BTCPayServer.Hosting
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{
app.UseMiddleware<BTCPayMiddleware>();
app.UseBTCPayOpenApi();
return app;
}
public static IApplicationBuilder UseHeadersOverride(this IApplicationBuilder app)

@ -1,9 +0,0 @@
using System;
namespace BTCPayServer.Hosting.OpenApi
{
public class IncludeInOpenApiDocs : Attribute
{
}
}

@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
using NSwag;
using NSwag.Generation.Processors.Security;
namespace BTCPayServer.Hosting.OpenApi
{
public static class OpenApiExtensions
{
public static IServiceCollection AddBTCPayOpenApi(this IServiceCollection serviceCollection)
{
return serviceCollection.AddOpenApiDocument(config =>
{
config.PostProcess = document =>
{
document.Info.Version = "v1";
document.Info.Title = "BTCPay Greenfield API";
document.Info.Description = "A full API to use your BTCPay Server";
document.Info.TermsOfService = null;
document.Info.Contact = new NSwag.OpenApiContact
{
Name = "BTCPay Server", Email = string.Empty, Url = "https://btcpayserver.org"
};
};
config.AddOperationFilter(context =>
{
var methodInfo = context.MethodInfo;
if (methodInfo != null)
{
return methodInfo.CustomAttributes.Any(data =>
data.AttributeType == typeof(IncludeInOpenApiDocs)) ||
methodInfo.DeclaringType.CustomAttributes.Any(data =>
data.AttributeType == typeof(IncludeInOpenApiDocs));
}
return false;
});
config.AddSecurity("APIKey", Enumerable.Empty<string>(),
new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description =
"BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen."
});
config.OperationProcessors.Add(
new BTCPayPolicyOperationProcessor("APIKey", AuthenticationSchemes.ApiKey));
config.TypeMappers.Add(
new PrimitiveTypeMapper(typeof(PaymentType), s => s.Type = JsonObjectType.String));
config.TypeMappers.Add(new PrimitiveTypeMapper(typeof(PaymentMethodId),
s => s.Type = JsonObjectType.String));
});
}
public static IApplicationBuilder UseBTCPayOpenApi(this IApplicationBuilder builder)
{
return builder.UseOpenApi()
.UseReDoc(settings => settings.Path = "/docs");
}
class BTCPayPolicyOperationProcessor : AspNetCoreOperationSecurityScopeProcessor
{
private readonly string _authScheme;
public BTCPayPolicyOperationProcessor(string x, string authScheme) : base(x)
{
_authScheme = authScheme;
}
protected override IEnumerable<string> GetScopes(IEnumerable<AuthorizeAttribute> authorizeAttributes)
{
var result = authorizeAttributes
.Where(attribute => attribute?.AuthenticationSchemes != null && attribute.Policy != null &&
attribute.AuthenticationSchemes.Equals(_authScheme,
StringComparison.InvariantCultureIgnoreCase))
.Select(attribute => attribute.Policy);
return result;
}
}
}
}

@ -71,8 +71,18 @@ namespace BTCPayServer.Hosting
// ScriptSrc = "'self' 'unsafe-inline'"
//});
})
.ConfigureApiBehaviorOptions(options =>
{
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity;
return builtInFactory(context);
};
})
.AddNewtonsoftJson()
#if DEBUG
#if RAZOR_RUNTIME_COMPILE
.AddRazorRuntimeCompilation()
#endif
.AddControllersAsServices();

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.JsonConverters
{
class DateTimeMilliJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(DateTime).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()) ||
typeof(DateTimeOffset).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()) ||
typeof(DateTimeOffset?).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
static DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
if (reader.TokenType == JsonToken.Null)
return null;
var result = UnixTimeToDateTime((ulong)(long)reader.Value);
if (objectType == typeof(DateTime))
return result.UtcDateTime;
return result;
}
private DateTimeOffset UnixTimeToDateTime(ulong value)
{
var v = (long)value;
if(v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return unixRef + TimeSpan.FromMilliseconds((long)v);
}
private long DateTimeToUnixTime(in DateTime time)
{
var date = ((DateTimeOffset)time).ToUniversalTime();
long v = (long)(date - unixRef).TotalMilliseconds;
if(v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return v;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
DateTime time;
if (value is DateTime)
time = (DateTime)value;
else
time = ((DateTimeOffset)value).UtcDateTime;
if (time < UnixTimeToDateTime(0))
time = UnixTimeToDateTime(0).UtcDateTime;
writer.WriteValue(DateTimeToUnixTime(time));
}
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

@ -23,6 +23,7 @@ namespace BTCPayServer.Models.InvoicingModels
public bool Replaced { get; set; }
public BitcoinLikePaymentData CryptoPaymentData { get; set; }
public string AdditionalInformation { get; set; }
}
public class OffChainPaymentViewModel

@ -24,9 +24,9 @@ namespace BTCPayServer.Models.InvoicingModels
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
}
public string HtmlTitle { get; set; }
public string CustomCSSLink { get; set; }
public string CustomLogoLink { get; set; }
public string HtmlTitle { get; set; }
public string DefaultLang { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();

@ -50,7 +50,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
[Required] public string StoreId { get; set; }
[Required]
[Range(double.Epsilon, double.PositiveInfinity, ErrorMessage = "Please enter a value bigger than zero")]
[Range(double.Epsilon, double.PositiveInfinity, ErrorMessage = "Please provide an amount greater than 0")]
public decimal Amount { get; set; }
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]

@ -19,6 +19,15 @@ namespace BTCPayServer.Models.StoreViewModels
public PaymentMethodId PaymentId { get; set; }
}
public SelectList CryptoCurrencies { get; set; }
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value;
}
public SelectList Languages { get; set; }
[Display(Name = "Default payment method on checkout")]
@ -57,14 +66,5 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
public bool RedirectAutomatically { get; set; }
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value;
}
}
}

@ -11,6 +11,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class DerivationSchemeViewModel
{
public DerivationSchemeViewModel()
{
}
@ -42,6 +43,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string AccountKey { get; set; }
public BTCPayNetwork Network { get; set; }
public bool CanUseHotWallet { get; set; }
public bool CanUseRPCImport { get; set; }
public RootedKeyPath GetAccountKeypath()
{

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Models.StoreViewModels
@ -43,5 +44,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string PayButtonText { get; set; }
public bool UseModal { get; set; }
public bool JsonResponse { get; set; }
public ListAppsViewModel.ListAppViewModel[] Apps { get; set; }
public string AppIdEndpoint { get; set; } = "";
public string AppChoiceKey { get; set; } = "";
}
}

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