Compare commits

..

689 Commits

Author SHA1 Message Date
bb4e92ec50 Fix alignement in Dynamic DNS 2019-07-25 19:44:40 +09:00
9218fb6463 bump 2019-07-25 19:39:15 +09:00
d9baea4c38 Remove global xpubs 2019-07-25 19:38:29 +09:00
6df6537cf9 Fix tests, improve logs 2019-07-25 19:36:03 +09:00
72d199f390 Add documentation link 2019-07-25 19:27:43 +09:00
233bce578b Can remove dyndns services 2019-07-25 19:07:56 +09:00
63472d54d7 Can configure multiple dynamic dns 2019-07-25 18:29:18 +09:00
db57b5ae80 Fix DynDNS renewal 2019-07-25 16:37:39 +09:00
8896d89908 Add Dynamic DNS support 2019-07-24 17:59:30 +09:00
8e07bf3ffb Update Login.cshtml ()
* Update Login.cshtml

[UI] Removed Unneeded string on login page with registering disabled. 

* Update Login.cshtml
2019-07-24 12:40:06 +09:00
6194d0ad44 bump NicolasDorier.RateLimits 2019-07-21 15:28:07 +09:00
138532d3d4 use donate.btcpayserver.org as donation link 2019-07-19 18:36:57 +09:00
4716b704d4 Paging on List Users page for big hosting providers like Esky () 2019-07-19 16:50:17 +09:00
109e576811 redo crowdfund modal perk list ()
* redo crowdfund modal perk list

closes 

* fix small responsive issue
2019-07-19 16:48:39 +09:00
631c878722 Fix typo. () 2019-07-19 16:47:52 +09:00
4cbcdb8af5 Make sure QRCodes use SVG instead of canvas (QRCodes on Tor does not work because of canvas fingerprinting protections) 2019-07-19 16:27:26 +09:00
d24628a386 fix payment request hub bug with payment types () 2019-07-18 14:00:46 +09:00
7e714bdfa2 Increasing default monitoring expiration time ()
With value 60 what would often happen is that invoice ends up being declared invalid, and then payment arrives later when system is not monitoring. After lots of production testing we decided to increase this value by default so that new users don't need to manually reassign invoices from `invalid` status, especially if they use third party plugin like wooCommerce.
2019-07-17 12:45:32 +09:00
e3283fb29b Bump NBitcoin and NBXplorer 2019-07-15 19:40:06 +09:00
be0285155f Do not redirect from login page if there is a returnUrl. 2019-07-15 17:18:30 +09:00
1c055a7282 Make sure perk contributions are not accounted until paid 2019-07-15 17:01:12 +09:00
8d3cdd39ca Fix "Configure Email Settings warning link not working" (Fix https://github.com/btcpayserver/btcpayserver/issues/906) 2019-07-14 22:54:27 +09:00
010ba4d5b6 Can specify display name for sender in email settings (Fix https://github.com/btcpayserver/btcpayserver/issues/910) 2019-07-14 22:45:14 +09:00
d176a16caa fix typo 2019-07-14 22:25:43 +09:00
fd4a27c1a3 When logged in, the URL /account/login is still accessible (Fix https://github.com/btcpayserver/btcpayserver/issues/916) 2019-07-14 22:16:23 +09:00
ae73858e23 Fix warning 2019-07-13 22:44:07 +09:00
1427e5458b Fix invoice page not showing tor link (Fix https://github.com/btcpayserver/btcpayserver/issues/915) 2019-07-13 22:42:46 +09:00
8853cf9f83 Only count contributors which paid invoice 2019-07-13 22:32:54 +09:00
de7f22bcbc bump 2019-07-13 22:21:23 +09:00
b8b2fa29d7 bump clightning 2019-07-13 22:03:33 +09:00
476a241936 bump 2019-07-12 23:36:28 +09:00
dc97982fad Remove trace of gdax (fix https://github.com/btcpayserver/btcpayserver/issues/913) 2019-07-12 23:34:10 +09:00
e488f93b17 Add explanation for the annoying windows popup 2019-07-12 13:04:54 +09:00
e6e9668bbb Prevent error 500 if bad psbt 2019-07-12 12:57:56 +09:00
56976898bd Fix error 414 2019-07-12 12:23:13 +09:00
221ff05c49 bump 2019-07-12 11:49:09 +09:00
8f719d3e33 Solve error 414 when PSBT are too big 2019-07-12 11:47:13 +09:00
67c2abca2d Hide openid warning message 2019-07-08 14:57:42 +09:00
36046f08f7 Use Migration startup task when starting BTCPay instead of hosted service. 2019-07-08 12:12:39 +09:00
3c4455c23c Update AppHubStreamer.cs ()
Bug fix 
2019-07-07 20:03:40 +09:00
e3db2e2b76 Remove warnings 2019-07-04 21:18:16 +09:00
5567a26b33 update translation 2019-07-04 21:16:20 +09:00
5387c3dd97 bump 2019-07-04 20:56:54 +09:00
d14eef979c Bump versions of package and software 2019-07-04 20:50:40 +09:00
c7069f4fd9 bump 2019-07-04 18:49:25 +09:00
b40239f93b bump nbxplorer 2019-07-04 18:48:54 +09:00
2958175add Sort payment requests, most recent first 2019-07-01 17:33:49 +09:00
719ad8f4d4 Add File Storage links ()
* add redirect to Services for enable provider

* add tooltip to FS FAQ
2019-07-01 15:20:33 +09:00
4055eda757 Part3: OpenIddict: Add Flows Event Handlers ()
* Part 1 & Part 2 squashed commits

pr changes


pr fixes


remove config for openid -- no need for it for now


Part 1: OpenIddict - Minor Changes & Config prep


Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies


pr changes


fix merge 


fix compile


fix compile 


Part 1: OpenIddict - Minor Changes & Config prep


add missing nuget


Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies

* Part3: OpenIddict: Add Flows Event Handlers

* pr changes

* fix merge

* fix rebase

* fix imports

* cleanup

* do not allow u2f enabled accounts to log in

* start better tests for flows

* add tests

* fixes

* reintroduce dynamic policy as policies on jwt do not work without it

* reduce logs

* fix incorrect endpoint definitions

* Add implicit flow e2e test

* add code flow and refresh flow

* do not allow jwt bearer auth for all requests( only those under /api)

* remove commentedt code

* make sure authorize attr is marked with scheme

* remove dynamic policy and set claims in jwt handler

* cleanup

* change serversettings policy to not need a claim

* Add test to checkadmin verification

* revert server setting claim removal

* fix test

* switch back to claim

* unit test fixes

* try fix build with weird references to csprojes

* start fixing rebase

* remove https requirement to handle tor

* reformat tests correctly

* fix csproj

* fix ut formatting

* PR Changes

* do not show selenium browser
2019-07-01 12:39:25 +09:00
442df56629 Merge pull request from Kukks/multiple-domains
Multiple domains for apps in BTCPay
2019-06-28 16:31:55 +09:00
477ab67fe9 set viewbag on method result instead 2019-06-26 07:02:22 +02:00
64c60741a0 Fix possible NRE 2019-06-26 13:46:45 +09:00
9e354d7703 Merge pull request from Kukks/pay-button-language
Allow language parameter in pay button endpoint
2019-06-26 13:27:52 +09:00
1932c1cd7c Merge pull request from Kukks/filesystem-downloads
fix tmp link download
2019-06-26 13:11:08 +09:00
11670d0c0f make checkout param more generic and add it to pay button generator 2019-06-25 21:01:37 +02:00
6cab02cd99 Multiple domains for apps in BTCPay
closes 
2019-06-25 20:41:32 +02:00
fc1d781272 fix tmp link download
closes 
2019-06-25 12:23:10 +02:00
a58ecfd35a Save local file storage upon selection instead
closes 
2019-06-25 11:40:33 +02:00
645516ee1b Change donation button from slider to input 2019-06-23 14:45:27 +09:00
f570de5086 Fix payment button alignement 2019-06-23 14:31:56 +09:00
81ccfa1e6c Change donation QR to use proper donation button. Remove also special thanks part as the contributors dramtically changed. 2019-06-23 14:29:43 +09:00
b808aa4971 allow language parameter in pay button endpoint 2019-06-18 18:31:20 +02:00
d1f1bc93b3 Fix version detection 2019-06-18 14:00:42 +09:00
faf433f644 Fix build 2019-06-18 13:51:04 +09:00
ba4660a03a bump 2019-06-18 13:41:30 +09:00
03aa3693d0 Update translations 2019-06-18 13:41:10 +09:00
ecae976993 Make sure we don't timeout on NBX 2019-06-18 13:37:24 +09:00
307c8980e0 Move Common and Version.csproj in Build folder 2019-06-17 21:42:48 +09:00
e53d0eda47 Fix NRE if the account has no rootedKeyPath 2019-06-16 12:32:00 +09:00
369b15b20b bump 2019-06-13 16:33:23 +09:00
ff8bbcd88a Merge branch 'amitasaurus-btcPay-coinSwitch' 2019-06-13 16:31:48 +09:00
c52d22dc30 resolved conflicts 2019-06-13 12:49:01 +05:30
4fa6c9dc3d coinswitchAmountDue always returning 1.025 possible fix 2019-06-13 12:14:28 +05:30
a958d10dd9 Fix local network detection (https://github.com/btcpayserver/btcpayserver-docker/pull/152) 2019-06-12 17:40:49 +09:00
ff86ce64b4 Merge pull request from Kukks/error-messages-login-register
Show Model errors on login/register
2019-06-12 14:02:26 +09:00
f31d8aa9d7 Merge pull request from btcpayserver/Kukks-patch-1
Fix automated docker build link
2019-06-12 14:01:18 +09:00
3cf7406123 Fix automated docker build link
Was pointing to nicolas' repo instead of the active one
2019-06-11 19:13:37 +02:00
5d8bf196a8 Fix: Allow get rate unauthenticated 2019-06-11 18:40:47 +09:00
019bd26c51 bump 2019-06-11 18:16:31 +09:00
0e1f924fc3 Relax "Insecure transport protocol to access this service, please use HTTPS or TOR" error in server setting services 2019-06-10 18:16:12 +09:00
15c3893aab Make sure currency is in uppercase 2019-06-10 00:46:29 +09:00
deeab7c238 Add link to checkout page theme doc 2019-06-09 22:26:59 +09:00
e5ba7b9e69 Refactor authentication handlers 2019-06-09 01:36:54 +09:00
ca5be7e38d Never use default AuthenticationScheme 2019-06-08 12:41:44 +09:00
fb530f2b34 fix build 2019-06-07 13:46:02 +09:00
29cbf63346 Remove deps on NetworkProvider in AppService 2019-06-07 13:40:48 +09:00
13c03cc0c2 Removing dependency on NetworkProvider from InvoiceWatcher 2019-06-07 13:34:38 +09:00
281280d3ec Fix crash which can happen during export if someone remove support for a network, inject Network inside paymentdata 2019-06-07 13:31:11 +09:00
410be51951 Update language 2019-06-07 00:49:05 +09:00
eefe8289b3 Fix exception in CreateInvoice if a payment method is not supported 2019-06-07 00:45:10 +09:00
a53a5944f8 Remove empty row if no validation 2019-06-06 18:54:51 +09:00
cd009466b6 Make sure we don't have empty row if no StatusMessage 2019-06-06 18:47:31 +09:00
f0c106de75 Change the menu nav bar pages by moving the title above the nav pills 2019-06-06 18:29:54 +09:00
fcf1b679e6 Show Model errors on login/register
Invalid logins and registrations were not showing any messages
2019-06-04 14:37:13 +02:00
03ba57cd46 bump 2019-06-04 10:24:51 +09:00
bea08e5cfd Refactor: Remove uneeded dependencies to PaymentMethodHandlerDictionary 2019-06-04 10:17:26 +09:00
01787e2662 Refactor: Remove PrepareInvoiceDTO 2019-06-04 10:11:52 +09:00
ac76220349 Move GetTransactionLink to PaymentType 2019-06-04 09:56:18 +09:00
796954c6e3 Refactor: Remove BlockExplorerLink from the payment handler 2019-06-04 09:52:06 +09:00
292c188182 Fix build errors 2019-06-04 09:40:36 +09:00
1f7097ef89 Refactor: Move DeserializeSupportedPaymentMethod to PaymentType 2019-06-04 09:33:42 +09:00
b97e083017 Refactor: Move DeserializePaymentMethodDetails to PaymentType 2019-06-04 09:22:46 +09:00
8ffd182b98 Refactor: Add DeserializePaymentData at the PaymentType level 2019-06-04 09:16:18 +09:00
1e77546251 Refactoring, make PaymentType a class instead of enum 2019-06-04 08:59:01 +09:00
8711960e74 Removing DeserializePaymentMethodDetails from IPaymentMethodHandler 2019-06-04 01:55:07 +09:00
8e2bcef824 The list of payment method should not depends on configuration of the users 2019-06-04 01:40:23 +09:00
d418cf7b07 Optimize docker files 2019-06-04 01:30:36 +09:00
864bcbb675 Move back GetCryptoPaymentData logic inside PaymentEntity 2019-06-04 01:24:15 +09:00
0b257b98f5 Move back ToPrettyString() in PaymentMethodId to fix crash if a payment method as been disabled 2019-06-04 01:06:03 +09:00
daab68d0b8 Merge pull request from btcpayserver/feature/new-register
New register form
2019-06-03 21:02:25 +09:00
ab0511aa1d Make is admin checkbox inline 2019-06-03 21:01:48 +09:00
12494c3ac6 Merge pull request from btcpayserver/feature/login
New login page
2019-06-03 20:57:06 +09:00
621533e050 New register form 2019-06-03 20:47:18 +09:00
dc334d230a New login windows 2019-06-03 20:36:07 +09:00
b848595378 Add missing command lines 2019-06-03 17:34:10 +09:00
ae4b2ab1fd Fix 2019-06-03 16:46:35 +09:00
2ca8cc6ca3 bump 2019-06-03 15:57:13 +09:00
3b57e2684e Add NotPaid_ExtraTransaction 2019-06-03 15:56:25 +09:00
898c672193 decrease number conf funding channel required for lightningd 2019-06-03 15:53:26 +09:00
18a7bc9278 Decrease number of confirmations requires for channels of lnd in tests 2019-06-03 15:51:13 +09:00
bb29ee10c5 Only execute external_tests on master 2019-06-03 15:41:44 +09:00
5441ae537a Fix docker-entrypoint for tests 2019-06-03 15:36:50 +09:00
0a0ddafd67 Add permission to run-tests 2019-06-03 15:34:09 +09:00
a3b914d8b4 Remove code 2019-06-03 15:32:46 +09:00
39f75d3742 Refactor test run by circleci 2019-06-03 15:32:20 +09:00
3dd77a4f2c Rename CircleCI steps and dockerfiles 2019-06-03 15:20:20 +09:00
6782e82972 Update translations 2019-06-02 18:13:04 +09:00
120fce0288 fix test container 2019-06-02 17:41:34 +09:00
aa57531ed7 fix test container 2019-06-02 17:37:44 +09:00
8f76bc0bcb Extract version in separate csproj to have better dockerfile caching 2019-06-02 17:33:35 +09:00
189280e602 Fix docker images 2019-06-02 17:06:00 +09:00
78ca26cf78 bump 2019-06-02 16:53:36 +09:00
0c5c6233c7 Add bisq as supporting P2P service 2019-06-02 16:30:44 +09:00
4fe480ee55 Fix selenium test 2019-05-31 15:40:59 +09:00
be6560e08c Merge pull request from rockstardev/uifixes
Tweaking UI styles, updating default table style, and extra notification for payment for paidPartial
2019-05-31 15:39:17 +09:00
0f58f6da36 fix cryptoimage 2019-05-31 08:00:32 +02:00
dcaf0463a7 Displaying notification for extra transaction if paidPartial 2019-05-31 07:48:42 +02:00
5c6643270b Bugfixing path to crypto image
Will need to move all these paths to absolute, rather than relative
2019-05-31 07:48:42 +02:00
7b337bde49 Restoring table border styling done by KayBeSee 2019-05-31 07:48:42 +02:00
7056aae301 Do not show custom amount field in cart when not enabled ()
fixes 
2019-05-31 14:29:16 +09:00
1b6eb9cab0 Update explorer to blockstream.info () 2019-05-31 14:26:41 +09:00
80e23beda9 Update TwentyTwenty 2019-05-31 14:16:17 +09:00
d70e120acc Fix test 2019-05-31 12:17:47 +09:00
c877937fdf Show the inputs of the PSBT in the review screen 2019-05-31 00:23:23 +09:00
8379b07de0 Use a redirect for update 2019-05-31 00:00:20 +09:00
c8c33245b8 Revert table changes from 2019-05-30 23:42:56 +09:00
0faf2fe83e Fix buttons in PSBT 2019-05-30 23:36:01 +09:00
19bc511f39 Can update PSBT, fix the PSBT review page 2019-05-30 23:16:05 +09:00
916323bb3b [WIP] Further abstractions to Payment Handlers ()
* mark items to abstract


wip


wip


wip


wip


wip


wip


wip


cleanup


parse other types


compile and fix tests


fix bug 


fix warnings


fix rebase error


reduce payment method handler passings


more cleanup


switch tests to Fast mode 


fix obsolete warning


remove argument requirement 


rebase fixes


remove overcomplicated code


better parsing


remove dependency on environement


remove async

* fixes and simplification

* simplify

* clean up even more

* replace nuglify dependency

* remove extra space

* Fix tests

* fix booboo

* missing setter

* change url resolver

* reduce payment method handlers

* wrap payment method handlers in a custom type

* fix tests

* make invoice controller UI selectlist population cleaner

* make store controller use payment handler dictionary

* fix ln flag

* fix store controller test

* remove null checks on payment handlers

* remove unused imports

* BitcoinSpecificBtcPayNetwork - abstract BTCPayNetwork

* some type fixes

* fix tests

* simplify fetching handler in invoice controller

* rename network base and bitcoin classes

* abstract serializer to network level

* fix serializer when network not provided

* fix serializer when network not provided

* fix serializer when network not provided

* Abstract more payment type specific logic to handlers

* fix merge issue

* small fixes

* make use of repository instead of direct context usage

* reduce redundant code

* sanity check

* test fixes
2019-05-30 16:02:52 +09:00
0e568e2af5 Make sure that only the log directory can be read on /server/logs 2019-05-30 11:46:09 +09:00
dde841383a Don't throw exception if derivation scheme is not found 2019-05-30 11:24:43 +09:00
81dae7d350 BTCPay Abstractions: Move PaymentMethod specific logic to their handlers () 2019-05-29 23:33:31 +09:00
d3e3c31b0c Btcpay abstract BTCPayNetwork -- Alternative PR to ()
* BitcoinSpecificBtcPayNetwork - abstract BTCPayNetwork

* some type fixes

* fix tests

* simplify fetching handler in invoice controller

* rename network base and bitcoin classes

* abstract serializer to network level

* fix serializer when network not provided

* fix serializer when network not provided

* fix serializer when network not provided

* try fixes for isolating pull request
2019-05-29 18:43:50 +09:00
90852fe951 updated styles on user server page ()
* updated styles on user server page

* moved files out of bootstrap.css

* removed old css classes from initial commit

* move css changes to site.css

* add missing }
2019-05-28 21:40:10 +09:00
a2251d245f Merge pull request from rockstardev/extendconfcount
Extending invoice monitoring time if we haven't reached MaxTrackedConfirmation
2019-05-26 11:18:22 +09:00
112f9c4241 Adding invoice back to pending to track confirmations if less than max 2019-05-25 17:30:27 -05:00
b300404bc7 Extending invoice monitoring if max confirmation count not reached 2019-05-25 17:20:17 -05:00
19161b52f5 Fiat denomination box disappeared from the wallet (fix ) 2019-05-25 22:23:32 +09:00
429170520e Make sure fingerprint/hdpath are passed down to AddDerivationScheme.
Close ledger popup on account selection.

Add additional info after pairing
2019-05-25 12:53:03 +09:00
5571413a78 Put Ledger Wallet pairing in a popup, prepare code for Trezor pairing ()
* Allowing for POS to be displayed at website root

* Switching to asp attributes for form post action

* Applying default formatting rules on HTML

* The destination pays mining fees => Subtract fees from amount

* small cleanup ()

* Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies ()

* Part 1: OpenIddict - Minor Changes & Config prep

* Part 1: OpenIddict - Minor Changes & Config prep

* Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies

* pr changes

* pr changes

* fix merge

* pr fixes

* remove config for openid -- no need for it for now

* fix compile

* fix compile 

* remove extra ns using

* Update Startup.cs

* compile

* adjust settings a bit

* remove duplicate

* remove external login provider placeholder html

* remove unused directives

* regenerate db snapshot model

* Remove dynamic policy

* Provide Pretty descriptions for payment methods from their handlers ()

* small cleanup

* Provide Pretty descriptions for payment methods from their handlers

* remove PrettyMethod()

* integration with trezor

* rough load xpub from trezor

* update deriv scheme trezor

* move ledger import to dialog

* add import from hw wallet dropdown

* Support temporary links for local file system provider ()

* wip

* Support temporary links for local file system provider

* pass base url to file services

* fix test

* do not crash on errors with local filesystem

* remove console

* fix paranthesis

* work on trezor.net integration

* pushed non compiling sign wallet code

* comment out wallet code

* abstract ledger ws in add deriv

* Auto stash before merge of "trezor" and "btcpayserver/master"

* final add changes

* cleanup

* improve connectivity and fix e2e tests

* fix selenium

* add experimental warning for trezor

* move import button to right and convert to text link

* switch to defer and async scripts in add deriv scheme

* make defer not async

* more elaborate import trezor dialog

* Fix small issues

* hide trezor for now
2019-05-25 11:45:36 +09:00
512ee16620 Refactor invoice entity to not have to inject the NetworkProvider () 2019-05-24 22:22:38 +09:00
15dc0d60db Split projects () 2019-05-24 18:42:22 +09:00
d86cc9192e Support temporary links for local file system provider ()
* wip

* Support temporary links for local file system provider

* pass base url to file services

* fix test

* do not crash on errors with local filesystem

* remove console

* fix paranthesis
2019-05-24 15:44:23 +09:00
25b08b21fa Provide Pretty descriptions for payment methods from their handlers ()
* small cleanup

* Provide Pretty descriptions for payment methods from their handlers

* remove PrettyMethod()
2019-05-24 15:38:47 +09:00
ef9c2e8af1 Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies ()
* Part 1: OpenIddict - Minor Changes & Config prep

* Part 1: OpenIddict - Minor Changes & Config prep

* Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies

* pr changes

* pr changes

* fix merge

* pr fixes

* remove config for openid -- no need for it for now

* fix compile

* fix compile 

* remove extra ns using

* Update Startup.cs

* compile

* adjust settings a bit

* remove duplicate

* remove external login provider placeholder html

* remove unused directives

* regenerate db snapshot model

* Remove dynamic policy
2019-05-24 15:17:02 +09:00
9bee48c601 small cleanup () 2019-05-24 15:11:38 +09:00
961942ff6a Merge branch 'posroot' 2019-05-24 15:08:49 +09:00
de1c2b0150 Allowing for POS to be displayed at website root ()
* Allowing for POS to be displayed at website root

* Switching to asp attributes for form post action

* Applying default formatting rules on HTML
2019-05-24 15:07:09 +09:00
5a73358bca The destination pays mining fees => Subtract fees from amount 2019-05-24 14:28:09 +09:00
1812ea90b5 Applying default formatting rules on HTML 2019-05-23 10:36:23 -05:00
d98a416ed9 Switching to asp attributes for form post action 2019-05-23 10:35:57 -05:00
b947749382 Allowing for POS to be displayed at website root 2019-05-22 14:30:47 -05:00
c4d0b061c9 bump 2019-05-21 19:06:27 +09:00
3bada5d443 Fix multi send substract fees 2019-05-21 19:04:39 +09:00
06a35787aa Make WalletSend match exactly the design of before without additional destination 2019-05-21 18:44:49 +09:00
88c931ec13 Make wallet able to send to multiple destinations ()
* Make wallet able to send to multiple destinations

* fix tests

* update e2e tests

* fix e2e part 2

* make headless again

* pr changes

* make wallet look exactly as old one when only 1 dest
2019-05-21 17:10:07 +09:00
3d436c3b0e Add Eclair to connection string examples ()
* Add Eclair to connection string examples

* bump LN nuget
2019-05-20 10:13:11 +09:00
b4bb44d3e6 Adding Paid filter, invoice confirmed/paid/complete () 2019-05-20 10:02:57 +09:00
39157e6883 Fix tests 2019-05-20 00:06:03 +09:00
9b6b9e6113 Fix build 2019-05-19 23:33:32 +09:00
87df34e064 Can actually upload PSBT file in PSBT Combine and PSBT view.
Validate transaction before allowing any broadcast and show errors nicely.
2019-05-19 23:27:18 +09:00
55a48ff84a Add some notice in sign with seed 2019-05-17 14:42:28 +09:00
aed98f16bb Show fee rate in transaction detail review 2019-05-16 12:56:06 +09:00
2926865c1b bump 2019-05-15 22:57:14 +09:00
20fb7fc188 Fixing bug induced with server converting UTC times to server local () 2019-05-15 22:02:39 +09:00
461462eafc fix file timezone and isDownload for local files temp urls ()
closes 
2019-05-15 22:02:03 +09:00
7aa6d1a8d7 fix test on Circle CI for selenium 2019-05-15 19:27:30 +09:00
d914fe2f48 Make sure that the accountkey can sign a transaction 2019-05-15 19:00:26 +09:00
e100edce24 Add selenium test for the manual seed signing 2019-05-15 16:00:03 +09:00
a68915d6cf WalletPSBTReady show the summary of the transaction, signing with the seed respect the keypath of the wallet settings 2019-05-15 15:00:09 +09:00
210d680b21 nameofify 2019-05-15 01:07:46 +09:00
8dc4acdc34 Make sure people does not use launchsettings by mistake 2019-05-15 01:06:26 +09:00
eb54a18fcd Wallet & PSBT: Sign with seed or key ()
* Allow signing a PSBT with an extkey/wif or mnemonic seed

* reword things

* small text
2019-05-15 01:03:48 +09:00
cf436e11ae Part 1: OpenIddict - Minor Changes & Config prep ()
* Part 1: OpenIddict - Minor Changes & Config prep

* add missing nuget

* pr changes

* pr fixes

* remove config for openid -- no need for it for now

* remove unused extension

* Add tests

* use pay tester http client

* check redirecturl in tests
2019-05-15 00:46:43 +09:00
e22b7f74c7 Increase test coverage of selenium 2019-05-14 23:33:46 +09:00
f8fca7434c Hack selenium again 2019-05-14 22:29:05 +09:00
66d303c4ba Install chrome driver on alpine 2019-05-14 19:58:28 +09:00
186ed8beb2 Hack selenium 2019-05-14 19:27:26 +09:00
fac546cc0b Print sources if Selenium test fail 2019-05-14 19:19:23 +09:00
9d2d2d0d64 Catch errors in AssertNoErrors 2019-05-14 19:13:55 +09:00
f58043b07f dump logs of selenium before failiing test 2019-05-14 19:09:26 +09:00
289c6fa10e Remove docker-compose update 2019-05-14 18:36:07 +09:00
00e24ab249 Make sure BTCPay is operational before starting tests 2019-05-14 18:35:22 +09:00
7b69e334d7 update docker-compose so selenium tests works 2019-05-14 18:05:16 +09:00
12507b6743 Add some logs related to selenium 2019-05-14 17:34:19 +09:00
3750842833 Show in logs where the btcpay tester is binding 2019-05-14 16:56:03 +09:00
1dcec3e1fb bind to all interface if inside the test container 2019-05-14 16:13:55 +09:00
d8e1edd6d3 bump nbx in tests 2019-05-14 16:07:46 +09:00
a1f1e90626 Make selenium work on CI 2019-05-14 16:06:51 +09:00
522d745883 Update NBXplorer and NBitcoin 2019-05-14 16:06:43 +09:00
8ffb81cdf3 Refactor Selenium tests to properly cleanup resources 2019-05-13 18:42:20 +09:00
ae5254c65e disable selenium test for a while 2019-05-13 18:14:31 +09:00
9d53888524 fix typo 2019-05-13 18:03:41 +09:00
cd6dd78759 Click around in selenium, and do not forget to run the selenium tests on circleCI 2019-05-13 17:59:15 +09:00
27fd49e61c Add --allow-admin-registration, useful for tests 2019-05-13 17:00:58 +09:00
a3a259556f Merge branch 'testcircleci' 2019-05-13 13:48:46 +09:00
803da75636 Only run integrations tests in btcpayserver repository 2019-05-13 13:48:09 +09:00
d1556eb6cd bump 2019-05-13 08:55:26 +09:00
a7edbfe5e9 Remove useless code 2019-05-13 08:23:24 +09:00
663b5beac1 remove useless code 2019-05-13 08:22:29 +09:00
7e164d2ec3 make sure we don't sign same input twice 2019-05-13 08:21:54 +09:00
f9fb0bb477 Simplify logic in LedgerHardwareWalletSerivce by using NBitcoin helper methods. 2019-05-13 08:18:12 +09:00
702c7f2c30 Fix tests 2019-05-13 00:35:06 +09:00
8b348ade75 Can select the signing key in WalletSettings 2019-05-13 00:30:28 +09:00
bf37f44795 Add Wallet settings menu, do not rebase keypaths when create the PSBT 2019-05-13 00:13:55 +09:00
698033b0cf Selenium Chrome Tests 2019-05-12 18:49:28 +09:00
10496363f5 Change button style on WalletPSBTReadyView 2019-05-12 16:19:27 +09:00
14647d5778 minor improvement to UI of PSBT 2019-05-12 15:16:40 +09:00
560dde3396 bump 2019-05-12 14:58:43 +09:00
7f9c2439c4 Custom date range filtering modal 2019-05-12 14:56:13 +09:00
6de5d0bce8 Unifying datetime styles across admin 2019-05-12 14:56:13 +09:00
c705a11aa7 Fixing merge bug with css file 2019-05-12 14:56:13 +09:00
45a196b407 Non-minified version of moment, adding required ref, fixing old ones 2019-05-12 14:56:13 +09:00
07cb6adb69 Extracting datetime flatpickr for use throught website 2019-05-12 14:56:13 +09:00
5358f81ce0 Dropdown for often used filterings 2019-05-12 14:56:13 +09:00
5b7988be79 Fixing display of long BOLT11 strings 2019-05-12 14:56:13 +09:00
e6c794d68f Moving update of confirmation count to InvoiceWatcher 2019-05-12 14:56:13 +09:00
de73fedd1b Check indicator after status change 2019-05-12 14:56:13 +09:00
2719849a54 bump 2019-05-12 14:51:57 +09:00
3011fecf0f Add tests for PSBT 2019-05-12 14:51:24 +09:00
6da0a9a201 Can combine PSBT 2019-05-12 13:13:52 +09:00
572fe3eacb Moveonly: Move all PSBT stuff in separate file 2019-05-12 11:13:04 +09:00
ff82f15246 Always rebase keys before signing, refacotring some code 2019-05-12 11:07:41 +09:00
b214e3f6df bump minimum version of ledger wallet 2019-05-12 01:35:13 +09:00
cb9130fdf9 Can broadcast PSBT, can decide to export something signed by the ledger via PSBT 2019-05-12 00:05:30 +09:00
925dc869a2 Add wasabi wallet to the wallet list supporting P2P connections 2019-05-11 22:25:10 +09:00
5f1aa619cd Can sign and export arbitrary PSBT 2019-05-11 20:26:31 +09:00
541c748ecb WalletSendLedger and LedgerConnection only depends on PSBT 2019-05-11 20:02:32 +09:00
e853bddbc8 Add utility tool to decode PSBT 2019-05-11 00:29:29 +09:00
79d26b5d95 Push rebase keypath and min fee logic down nbxplorer 2019-05-10 19:30:10 +09:00
840f52a75b Fix build 2019-05-10 14:36:57 +09:00
f955302c74 remove CF modal text 2019-05-10 11:35:51 +09:00
95e7d3dfc4 Don't scan 49' or 84' if not segwit 2019-05-10 10:55:10 +09:00
75f2749b19 Decouple HardwareWalletService into two classes: LedgerHardwareWalletService and HardwareWalletService 2019-05-10 10:48:30 +09:00
01e5b319d1 Save the fingerprint of the root of LedgerWallet, and use it. Simplify HardwareWallet 2019-05-10 01:05:37 +09:00
e504163bc7 Add NonAction to CreatePSBT 2019-05-09 19:34:45 +09:00
aba3f7d6bd bump 2019-05-09 19:21:03 +09:00
8d74023d30 update translation 2019-05-09 19:20:36 +09:00
602625fc17 Fix tests 2019-05-09 19:14:01 +09:00
bbeb2d5009 Refactor ElectrumMapping with proper enum 2019-05-09 19:05:08 +09:00
51faa39636 Add some tests to check that AccountKeyPath and RootFingerprint are taken into account during PSBT creation 2019-05-09 18:58:14 +09:00
f37bfbf9f9 Add more tests 2019-05-09 18:38:25 +09:00
ba9928831e Fix tests 2019-05-09 18:11:39 +09:00
2b6bd3d751 Assume ElectrumMapping of BTC if not specified 2019-05-09 17:51:46 +09:00
e96ca21c89 Small refactoring for tests 2019-05-09 17:21:51 +09:00
6ee10fe98b add grs 2019-05-09 17:16:17 +09:00
a567c19759 conditionally select electrum mapping based on network 2019-05-09 17:16:17 +09:00
bb3a087d39 Move ElectrumMapping to BtcPayNetwork 2019-05-09 17:16:17 +09:00
5a92fe736f Fix: Uploading coldcard in derivation scheme would forget to remember some data 2019-05-09 16:11:09 +09:00
88390402a4 reorder buttons 2019-05-09 12:48:11 +09:00
538eb66672 Allow import of coldcard wallet 2019-05-09 12:48:11 +09:00
0b6dfe0fd3 Fix DerivationSchemeSettings.ToPrettyString() 2019-05-09 01:07:05 +09:00
d5579ef2b5 Do not serialize PaymentId for DerivationSchemeSettings 2019-05-09 01:06:03 +09:00
836c3a5b3a Make sure we don't confuse user between derivation scheme of coldcard or btcpay 2019-05-09 00:55:49 +09:00
f2da64adad Add parsing of cold card wallet 2019-05-09 00:55:48 +09:00
e5704abfb3 Fix migration from old version to new version of WalletKeyPathRoots 2019-05-09 00:55:48 +09:00
3bf4eea1fe Improve error handling for export psbt 2019-05-09 00:55:47 +09:00
aa23222339 CreatePSBT should always rebase the PSBT 2019-05-09 00:55:46 +09:00
68c1670c70 Show pretty wallet string in Update Store 2019-05-09 00:55:45 +09:00
914eaaaa51 Refactor DerivationStrategy to DerivationSchemeSettings 2019-05-09 00:55:44 +09:00
5831ba2143 bump 2019-05-09 00:23:52 +09:00
c167a24f09 use older version of lib until it supports linux better 2019-05-08 20:17:17 +09:00
a539d27c62 fix exception handling 2019-05-08 20:17:17 +09:00
d7fc079376 RBF on by default, can disable it in Wallet Send /advanced settings. 2019-05-08 15:24:20 +09:00
3a05f7e294 PSBT export support in send from wallet screen 2019-05-08 14:40:16 +09:00
03713f9bd8 Add PSBT support in the send screen 2019-05-08 14:39:37 +09:00
2a145f4350 Replace noob button in wallet send by an advanced settings accordion 2019-05-08 12:34:33 +09:00
d049da696c Fix exception thrown if user does not exist on login 2019-05-08 12:34:13 +09:00
5a46d0e80d Add cmd tools to generate blocks 2019-05-08 12:19:16 +09:00
926250a967 Remove warnings 2019-05-07 23:34:31 +09:00
139b588795 fix coinswitch...yet again 2019-05-07 23:23:29 +09:00
909f18f9c7 Update language 2019-05-07 18:02:14 +09:00
f598495198 bump 2019-05-07 17:57:04 +09:00
95d746504d Changing invoice state and updating display from js 2019-05-07 17:29:19 +09:00
9a2e1d43ea Triggering optional confirmation update only on Invoice details page 2019-05-07 17:29:19 +09:00
be844978c1 Allow cancelling a non paid pending invoice in payment requests ()
* allow cancel on un paid new invoices in payment requests

* start work on cancel pr payment

* finish up cancel action

* final touch and add tests
2019-05-07 17:26:40 +09:00
60a361f963 Trying to make sure Azure tests does not run on PRs 2019-05-07 17:11:23 +09:00
93f50451e6 bump deps 2019-05-07 17:05:45 +09:00
0936812df0 Fix date time issues on crowdfund.payment requests ()
* fix some conditional display bugs in crowdfund

* bump flatpickr

* make clear button show up even with flatpickt fake input ui

* update uis to specify date value in specific format and use custom format for flatpickr display and use moment to parse date instead

* fix remaining public ui date issues
2019-05-07 17:01:37 +09:00
50351f56f8 Fix grammar ()
* Fix grammar

* Fix typo
2019-05-07 16:55:22 +09:00
232ceed8b0 Prettify the bitcoin core node page 2019-05-07 14:44:26 +09:00
b6c37a73b1 Fix duplicated entries on Services. Fix formatting of P2P page. 2019-05-07 14:31:49 +09:00
5967666df6 Add green wallet info 2019-05-07 14:16:44 +09:00
bf035333cf Add service type P2P 2019-05-07 14:07:36 +09:00
f93d1173e2 Show tor exposed bitcoin node 2019-05-07 13:58:55 +09:00
08bf4faeee Pass the hint change address to hardware wallet (useful in care of send-to-self where the underlying wallet support only output belonging to self) 2019-05-07 08:21:34 +09:00
e2b2cf0175 Do not drop column in u2f migration if not possible ()
closes 
2019-05-05 00:57:44 +09:00
d32a24004e Fix test 2019-05-03 12:59:11 +09:00
1f04e4e6be Add rpcport for bitcoin-cli 2019-05-03 11:10:01 +09:00
778dcf97b1 update docker compose for bitcoin 2019-05-03 11:04:19 +09:00
957fbdb907 Update NBitcoin, NBXplorer, Bitcoin Core 2019-05-03 10:18:08 +09:00
e169b851ee Remove another warning 2019-05-02 21:44:16 +09:00
7fadb4c5ad Remove some annoying warnings 2019-05-02 21:38:39 +09:00
a20db7f341 bump nbx 2019-05-02 21:35:28 +09:00
b5f4739ae5 Allow invoice creation to only allow specific payment methods in UI ()
* allow invoice creation to only allow specific payment methods

* add test

* reuse existing feature

* final fixes
2019-05-02 21:29:51 +09:00
4bc03fbf06 Code coloring invoice states 2019-05-02 21:11:56 +09:00
1d3ff143d2 Tweaking UI, expanding details and max width on order id 2019-05-02 21:11:56 +09:00
6918b8a291 Extracting payment details population, refactoring invoice data load 2019-05-02 21:11:56 +09:00
3cd37682d3 [BUG FIX]: Coinswitch exchange with altcoins popup not showing bug fix () 2019-05-02 21:02:01 +09:00
19a990b095 Add U2f Login Support ()
* init u2f

* ux fixes

* Cleanup Manage Controller

* final changes

* remove logs

* remove console log

* fix text for u2f

* Use Is Secure instead of IsHttps

* add some basic u2f tests

* move loaders to before title

* missing commit

* refactor after nicolas wtf moment
2019-05-02 21:01:08 +09:00
87a4f02f18 bump NBXplorer 2019-05-02 20:46:27 +09:00
8a99fc0505 Fix Azure Storage () 2019-05-02 20:39:12 +09:00
bac99deb6c Do not run external integration if PR 2019-05-02 20:38:45 +09:00
e65850b1eb Refactor Send money from ledger using PSBT 2019-05-02 18:56:01 +09:00
77338c6054 [BUG FIX]: Coinswitch exchange with altcoins popup not showing bug fix 2019-05-02 14:49:33 +05:30
a6e52ed3df bump NBitcoin 2019-05-02 17:31:57 +09:00
4a9eadf71a Bump NBXplorer 2019-05-02 17:28:54 +09:00
b8f6cf4f23 Execute ExternalIntegration tests after 2019-05-02 15:31:51 +09:00
e8abc1137b remove duplicate view code for email and fix password bug ()
closes 
2019-05-01 12:17:25 +09:00
8507688c50 add azure storage config validation () 2019-05-01 12:16:55 +09:00
5718096224 Revert "Merge branch 'sonarqube'"
This reverts commit d76e61e6f468b006659eebeff84be8bdedc02822.
2019-04-29 07:53:34 +02:00
d76e61e6f4 Merge branch 'sonarqube' 2019-04-29 07:21:50 +02:00
232817c00d add sonarqube 2019-04-29 07:18:21 +02:00
86af585df3 bump nbx in tests 2019-04-29 12:31:21 +09:00
9e770ea484 bump dbriize 2019-04-29 12:30:47 +09:00
9670f11554 Fix HTTP 500 errors if querying the website in tests 2019-04-28 23:11:24 +09:00
dc369d52cb Use fa fa-user for profile menu item 2019-04-28 16:07:42 +09:00
33c755fc54 Replace log out text by an icon 2019-04-28 15:59:44 +09:00
c5adc0eb71 Rename ShowEmailWarningForStore(storeId) => IsEmailConfigured(storeId) 2019-04-28 15:28:22 +09:00
fcb1de8a86 Show email warning on apps when settings are not complete ()
* Show email warning on apps when settings are not complete

closes 

* refactor email warning logic
2019-04-28 15:27:10 +09:00
6df83ad148 Replace DBreeze by DBriize 2019-04-28 15:16:11 +09:00
857a436677 Clarifying comma is required for splitting params, providing example 2019-04-26 22:00:12 -05:00
c6091750b0 Displaying switchable datetimes on wallet transactions page 2019-04-26 22:00:12 -05:00
64e7324285 Fixing CanUsePoSApp test 2019-04-26 22:00:12 -05:00
d5bd0ee781 Filtering invoices by StartDate and EndDate
Now it's required to separate parameters with comma. Forced to do
this because dates have spaces between date and time part
2019-04-26 22:00:12 -05:00
3b91b38014 do not run external integration tests if in a PR 2019-04-24 22:40:36 +09:00
165d4e2732 remove unused parameter 2019-04-24 22:40:36 +09:00
098dfacce8 Remove segwit limitation for rescan 2019-04-24 22:40:35 +09:00
44d1419af9 Add itemCode to Invoice Response ()
closes 
2019-04-24 22:36:35 +09:00
d0d077642d Make sure we returns only one token in GetTokens 2019-04-23 16:05:11 +09:00
dc04839fab Run Azure tests in CircleCI 2019-04-22 17:19:04 +09:00
4ce0cb4b35 Remove useless code in storage providers 2019-04-22 16:57:22 +09:00
5100c36c06 Uncomment google/amazon code, just disable it in the service registration 2019-04-22 16:45:50 +09:00
b184360eb7 Abstracted cloud storage - Amazon/Google/Azure/Local ()
* wip

* add in storage system

* ui fixes

* fix settings ui

* Add Files Crud UI

* add titles

* link files to users

* add migration

* set blob to public

* remove base 64 read code

* fix file query model init

* move view model to own file

* fix local root path

* use datadir for local storage

* move to services

* add direct file url

* try fix tests

* remove magic string

* remove other magic strings

* show error message on unsupported provider

* fix asp net version

* redirect to storage settings if provider is not supported

* start writing tests

* fix tests

* fix test again

* add some more to the tests

* more tests

* try making local provider work on tests

* fix formfile

* fix small issue with returning deleted file

* check if returned data is null for deleted file

* validate azure Container name

* more state fixes

* change azure test trait

* add tmp file url generator

* fix tests

* small clean

* disable amazon and  google


comment out unused code for now


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

* Add Zeus

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

* remove currency placeholder in payment requests

* Improve date picker ui 

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

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

closes 

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

closes 

* Do not save clone instantly
2019-03-07 14:27:16 +09:00
2b567de5c1 Allow sounds and animation colors in crowdfund to be configured ()
closes 
2019-03-07 14:25:09 +09:00
ef46d03760 fix lightning typo ()
closes 
2019-03-07 13:14:47 +09:00
b174f299fa Fix LND QR code (Fix ) 2019-03-07 10:26:27 +09:00
09837966b9 bump 2019-03-06 17:53:03 +09:00
465dce1d02 Running check of submitted SMTP data on both Test and Save 2019-03-05 13:00:14 -06:00
067dbad546 Fix build 2019-03-05 17:29:52 +09:00
522970fdb9 Fix build 2019-03-05 17:21:44 +09:00
e67aa499a6 Fix build 2019-03-05 17:20:26 +09:00
3b68d81507 Small refactoring 2019-03-05 17:13:34 +09:00
051248f2fc Add CancellationToken to GetRatesAsync and propagate it from the controllers to the rate fetcher 2019-03-05 17:09:17 +09:00
9a239f99f4 Add bylls as supported exchange 2019-03-05 16:07:23 +09:00
86c431d66e Only get contributions from invoices matching the currency of the Payment request or crowdfund 2019-03-05 14:26:27 +09:00
b35fe0e8e3 Fix CurrentPendingAmount/CurrentAmount not being set 2019-03-05 14:06:40 +09:00
54905f5ceb Fix currencyValue 2019-03-05 13:58:13 +09:00
1c9b05d992 Fix: Payment requests and crowdfund were estimating current contributions based on the current rate 2019-03-05 13:54:34 +09:00
a89c71df38 Fix bundleconfig 2019-03-04 22:38:06 +09:00
3db1f2af12 Fix warning when compiling bundles 2019-03-04 22:35:56 +09:00
5399ff2751 Fix forwarding options 2019-03-04 22:34:14 +09:00
bcea6027e9 Replace Forwarded Headers via ASP.NET Core middleware 2019-03-04 20:48:19 +09:00
a9722df7e4 bump 2019-03-04 18:39:16 +09:00
474be6f7be French update 2019-03-04 18:39:03 +09:00
ada9a7264b Fix typo in AppsController.cs ()
Fixing a tiny typo in the AppsController.cs
2019-03-04 18:36:47 +09:00
e991b302d0 Fix bug: "BTCPay is expecting you to access this website" being cached 2019-03-04 18:33:57 +09:00
eef301c6ec Fix error message if X-Forwarded-Proto not set correctly 2019-03-04 17:26:08 +09:00
3fdfd0adfd Removed exponentiation on invoice request amount
Fixes 
2019-03-03 17:44:28 -06:00
358f1ffc43 Removed border when there is only one currency 2019-03-03 17:44:28 -06:00
349c3409df Preserve password when sending test email 2019-03-03 17:40:40 -06:00
0263a2950c added enabled to ViewCrowdfundViewModel, added warning on preview page 2019-03-03 17:39:53 -06:00
0364a57cae Added a space between Enable and SSL. 2019-03-03 17:39:22 -06:00
e232dd7d7e Add a space between "Test" and "Email" in the UI. 2019-03-03 17:38:57 -06:00
fee936b569 Only checking for last admin if user being deleted is admin
Bugfixing issue 
2019-03-03 17:38:38 -06:00
420115c54d bump nbx 2019-03-03 01:37:34 +09:00
312e961098 Update c-lightning, nbxplorer and lightning libs 2019-03-02 23:38:26 +09:00
223213857f Do not expose internal IP on SSH connection settings 2019-03-01 16:41:36 +09:00
c0da81557b Fix missing authenticated URI for charge 2019-03-01 15:38:11 +09:00
81945c0737 Fix bug on spark external config parsing 2019-03-01 15:34:30 +09:00
9664e3d6a1 bump version and deps 2019-03-01 14:55:53 +09:00
0a8bd38e76 Danish support 2019-03-01 14:52:45 +09:00
013054fb82 Small refactor 2019-03-01 14:46:32 +09:00
d898f716d1 Add some tests on externalConnectionString 2019-03-01 14:33:32 +09:00
1d3f144d21 Support relative path for external services, simplify the code in Services 2019-03-01 13:20:21 +09:00
de29d87487 Fix QR code not showing full uri if using relative path 2019-02-28 23:01:25 +09:00
ebef085a9c Support relative path for Spark and RTL external url, check in server settings if we are using a secure protocol 2019-02-28 22:20:14 +09:00
2c1f159d72 Document error of reverse proxy configuration 2019-02-28 21:43:44 +09:00
1ef59e05a5 fix dockerfile 2019-02-27 21:58:28 +09:00
5e09992637 Fix dockerfile 2019-02-27 21:56:36 +09:00
30448233b1 Fix dockerfile 2019-02-27 21:49:25 +09:00
a1601a17aa Fix permission 2019-02-27 21:44:16 +09:00
7522f7d0f7 fix remaining edgecase with payment request pay endpoint () 2019-02-27 21:42:25 +09:00
d04b9c4c09 Remove last reference to externalurl 2019-02-27 21:41:02 +09:00
1a24ff9a49 bump 2019-02-27 21:41:01 +09:00
13d72de82d fix payment request redirect url () 2019-02-27 20:25:13 +09:00
3728fdab3f improve warning message 2019-02-27 18:54:19 +09:00
2317e3d50c Make sure we rewrite the request scheme 2019-02-27 18:52:11 +09:00
5f15976c02 bump 2019-02-27 18:46:15 +09:00
7f592639c5 Remove URI rewritting and ExternalUri stuff 2019-02-27 18:38:11 +09:00
a98402af12 Making currency switching indicator more obvious with button style () 2019-02-27 13:45:58 +09:00
316ffa91d1 bump 2019-02-26 23:10:58 +09:00
c24953b57e Making hamburger light to see it on dark background () 2019-02-26 14:08:03 +09:00
7a1b1b7e5e Merge branch 'payment-requests' 2019-02-25 17:59:02 +09:00
70f71f64c4 Use internal tags, not order id in the streamer to know if the incoming invoice is for the payment request 2019-02-25 17:56:29 +09:00
5bccd07d7d Make sure the invoiceEvent is from a payment request. 2019-02-25 17:56:29 +09:00
d818baa6d1 Fix crowdfund test 2019-02-25 17:56:29 +09:00
249b8abf03 deduct network from contributions + removed unsued Enabled properties 2019-02-25 17:56:29 +09:00
c134277514 remove creating state from payment requests 2019-02-25 17:56:29 +09:00
f5d366cf7f Fix final bugs 2019-02-25 17:56:29 +09:00
ad25a2ed08 Add payment requests 2019-02-25 17:56:28 +09:00
1e7a2ffe97 Enable/Disable tips and discount. Fix custom amount. () 2019-02-25 15:11:03 +09:00
dd52075ff1 Add backoff delay if fetching exchange rate is failing 2019-02-24 22:00:30 +09:00
0253e42bd5 Do not poll in the invoice page if websocket are working 2019-02-23 15:22:17 +09:00
d99774f8d9 fix tests 2019-02-22 22:52:43 +09:00
d563a2ec89 Fix tests 2019-02-22 22:48:39 +09:00
b4b4523193 Round currency up to significant decimal 2019-02-22 22:15:25 +09:00
fbcb69f447 Do not prevent btcpayserver from starting if using insecure protocol for lightning services 2019-02-22 18:24:27 +09:00
8ae5a9c1f7 Fix old crowdfunding invoices 2019-02-22 17:51:38 +09:00
3ef5bfb6eb bump 2019-02-22 17:30:54 +09:00
4016ded584 Affect orderId to crowdfund app invoices 2019-02-22 17:29:54 +09:00
5b0b4adb1c Fix service link for RTL 2019-02-22 15:46:43 +09:00
f1ec3b0c75 bump 2019-02-22 15:08:45 +09:00
b5d55a2066 Add RTL support 2019-02-22 15:06:52 +09:00
0d2c9fe377 Fix https://github.com/btcpayserver/btcpayserver/issues/585 2019-02-22 13:52:35 +09:00
2c7cc9a796 Fix: invoice Price was not being rounded if no taxIncluded present 2019-02-21 21:58:49 +09:00
2e1d623755 fix https://github.com/btcpayserver/btcpayserver/issues/596 2019-02-21 21:30:30 +09:00
52fee8f842 Make sure no nullreferenceexception is thrown if invalid invoice 2019-02-21 19:36:05 +09:00
6ba17e8e30 Can filter supported payment methods for an invoice 2019-02-21 19:34:11 +09:00
ac3432920a Fix build 2019-02-21 18:42:12 +09:00
63c88be533 Use CreateInvoiceRequest instead of NBitpay Invoice type 2019-02-21 18:40:27 +09:00
3cb577e6ba Add link back to official website 2019-02-21 14:04:03 +09:00
1e0d64c548 Improve homepage, document mattermost and point on the official website. 2019-02-21 13:50:46 +09:00
bc1b9ff59c update translations 2019-02-20 23:16:13 +09:00
7d73bed3be bump 2019-02-20 23:06:52 +09:00
126fbdfd60 Fix null reference exception if the NotificationUrl is not set 2019-02-20 23:03:04 +09:00
15094436fd bump lnd 2019-02-20 21:29:16 +09:00
010c653995 Create EventHostedServiceBase and make AppHubStreamer use this 2019-02-20 12:27:10 +09:00
119f82fd4e Properly aggregate contributions amount 2019-02-19 16:15:14 +09:00
3bbf4de5d2 Fix live update of crowdfunding, add tests, consider payments as confirmed if invoice is confirmed 2019-02-19 16:01:28 +09:00
0807f3b87b Remote internal tags at store level 2019-02-19 13:24:04 +09:00
4e9b3b40aa Fix crowdfunding-admin js file not being included 2019-02-19 13:20:06 +09:00
cc444811db Rename CrowdfundHubStream to AppHubSteamer 2019-02-19 13:18:30 +09:00
50c8525012 Moving CrowdfundSettings in its own file 2019-02-19 13:07:10 +09:00
aedad497e8 Rename AppsHelper to AppService 2019-02-19 13:04:58 +09:00
b1b231e645 Add tests on tagging 2019-02-19 12:59:12 +09:00
dc46fd225a Migrate old crowdfund deployment to the new tagging system 2019-02-19 12:53:24 +09:00
6226de7cff Refactor Crowdfund to use the tagging system 2019-02-19 12:48:48 +09:00
37327ec674 Apps can tag invoices 2019-02-19 12:48:08 +09:00
c071c81403 Pass the whole Entity object to internal InvoiceEvent 2019-02-19 12:08:07 +09:00
85d75a013a The invoices link of crowdfund show all invoices of the store if it is set to use all store's invoice 2019-02-19 11:45:04 +09:00
3816b36131 Add internal tags to invoice 2019-02-19 11:14:21 +09:00
dc7965267b Use GetRelativePathOrAbsolute in ViewCrowdfund and ViewPointOfSale 2019-02-19 00:28:44 +09:00
ce9a6bced7 Use GetRelativePathOrAbsolute in ShowLightningNodeInfo 2019-02-18 12:25:14 +09:00
85325dc710 Update translations 2019-02-18 12:24:55 +09:00
ac4050df70 Improve the UI of lightning node info 2019-02-17 19:40:39 +09:00
a16a53167b Can put lightning node info inside an XFrame 2019-02-17 19:30:16 +09:00
afab3cf847 Better Datetime picker picker in crowdfund page 2019-02-17 19:25:18 +09:00
8fdaeb7bac Fix race condition on calculation of contributions, refactor the methods to AppHelper 2019-02-17 19:17:59 +09:00
7e0f9f6e0d Inject HtmlSanitizer in AddBTCPayServer, remove AppHelpers deps when possible 2019-02-17 18:47:25 +09:00
5b1bf6cd88 add email to export () 2019-02-17 18:33:40 +09:00
b1584c352b Free some memory 2019-02-17 16:13:16 +09:00
b06b83503c Better status message 2019-02-15 10:05:29 -06:00
b03d89c190 Different message for admin deletion, check not to delete last admin
Ref: , 
2019-02-15 10:05:29 -06:00
f53548d10f Showing warning when user tries to delete last admin 2019-02-15 10:05:29 -06:00
5ec2f54d7f Merge pull request from BenSanex/bugfix/591_FixValidationMessage
Custom validation message for Crowdfund form primary currency
2019-02-15 10:03:05 -06:00
db588ff961 I've added asterisk. Isn't that impressive? 2019-02-15 10:02:17 -06:00
2b7006a14c add asterisk, revert primary currency error message, remove the 2019-02-11 21:53:45 -06:00
8f5f07882f Custom validation message for Crowdfund form primary currency 2019-02-07 20:27:26 -06:00
0eee8e7464 Returns Access-Control-Allow-Origin * on all Bitpay GET and post requests. 2019-02-02 16:12:51 +09:00
3725a5b644 Correctly set Access-Control-Allow-Headers 2019-02-02 15:51:38 +09:00
c84c0ac64d set CORS headers 2019-02-02 15:22:00 +09:00
098e07988c Bypass MVC for replying to CORS requests if Bitpay API 2019-02-02 15:19:22 +09:00
66bb702aca Fix CORS for bitpay API again 2019-02-02 13:58:32 +09:00
03ff2fedf0 Update Translator grammar () 2019-02-01 17:35:49 +09:00
c707f47b11 bump 2019-01-31 22:03:46 +09:00
585efa3ff5 Fix: Default payment method should not return a disabled one 2019-01-31 22:03:28 +09:00
516 changed files with 42288 additions and 7720 deletions
.circleci
.github/ISSUE_TEMPLATE
BTCPayServer.Common
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
Authentication
BTCPayServer.csproj
Configuration
Controllers
Crowdfund
Data
DerivationSchemeParser.csDerivationSchemeSettings.csDerivationStrategy.cs
Events
ExplorerClientProvider.csExtensions.cs
Extensions
HostedServices
Hosting
IStartupTask.cs
JsonConverters
Logging
MigrationStartupTask.cs
Migrations
ModelBinders
Models
PaymentRequest
Payments
Program.cs
Properties
SearchString.cs
Security
Services
Storage
U2F
Validation
Views
Account
Apps
AppsPublic
Home
Invoice
Manage
PaymentRequest
PublicLightningNodeInfo
Server
Shared
Stores
UserStores
ViewsRazor.cs
Wallets
bundleconfig.json
wwwroot
Build
Dockerfile.linuxamd64Dockerfile.linuxarm32v7README.mdamd64.Dockerfilearm32v7.Dockerfilebtcpayserver.slndocker-entrypoint.shpublish-docker.ps1

@ -1,25 +1,41 @@
version: 2
jobs:
build:
machine:
docker_layer_caching: true
steps:
- checkout
test:
fast_tests:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
cd BTCPayServer.Tests
docker-compose down --v
docker-compose build
docker-compose run tests
cd .circleci && ./run-tests.sh "Fast=Fast"
selenium_tests:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
cd .circleci && ./run-tests.sh "Selenium=Selenium"
integration_tests:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
cd .circleci && ./run-tests.sh "Integration=Integration"
external_tests:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
cd .circleci && ./run-tests.sh "ExternalIntegration=ExternalIntegration"
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:
amd64:
machine:
docker_layer_caching: true
steps:
@ -28,11 +44,11 @@ jobs:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 .
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
publish_docker_linuxarm:
arm32v7:
machine:
docker_layer_caching: true
steps:
@ -42,11 +58,11 @@ jobs:
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
publish_docker_multiarch:
multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
@ -69,11 +85,17 @@ workflows:
version: 2
build_and_test:
jobs:
- test
- fast_tests
- selenium_tests
- integration_tests
- external_tests:
filters:
branches:
only: master
publish:
jobs:
- publish_docker_linuxamd64:
- amd64:
filters:
# ignore any commit on any branch by default
branches:
@ -81,16 +103,16 @@ workflows:
# only act on version tags
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_linuxarm:
- arm32v7:
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_multiarch:
- multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm
- amd64
- arm32v7
filters:
branches:
ignore: /.*/

8
.circleci/run-tests.sh Executable file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
cd ../BTCPayServer.Tests
docker-compose -v
docker-compose down --v
docker-compose build
docker-compose run -e "TEST_FILTERS=$1" tests

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

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

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

@ -3,13 +3,18 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer
{
public enum DerivationType
{
Legacy,
SegwitP2SH,
Segwit
}
public class BTCPayDefaultSettings
{
static BTCPayDefaultSettings()
@ -38,13 +43,69 @@ namespace BTCPayServer
public string DefaultConfigurationFile { get; set; }
public int DefaultPort { get; set; }
}
public class BTCPayNetwork
public class BTCPayNetwork:BTCPayNetworkBase
{
public Network NBitcoinNetwork { get; set; }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public bool SupportRBF { get; internal set; }
public string LightningImagePath { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
public KeyPath GetRootKeyPath(DerivationType type)
{
KeyPath baseKey;
if (!NBitcoinNetwork.Consensus.SupportSegwit)
{
baseKey = new KeyPath("44'");
}
else
{
switch (type)
{
case DerivationType.Legacy:
baseKey = new KeyPath("44'");
break;
case DerivationType.SegwitP2SH:
baseKey = new KeyPath("49'");
break;
case DerivationType.Segwit:
baseKey = new KeyPath("84'");
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
return baseKey
.Derive(CoinType);
}
public KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
public override T ToObject<T>(string json)
{
return NBXplorerNetwork.Serializer.ToObject<T>(json);
}
public override string ToString<T>(T obj)
{
return NBXplorerNetwork.Serializer.ToString(obj);
}
}
public abstract class BTCPayNetworkBase
{
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
public Money MinFee { get; internal set; }
public string DisplayName { get; set; }
[Obsolete("Should not be needed")]
@ -57,23 +118,22 @@ namespace BTCPayServer
}
public string CryptoImagePath { get; set; }
public string LightningImagePath { get; set; }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
public override string ToString()
{
return CryptoCode;
}
internal KeyPath GetRootKeyPath()
public virtual T ToObject<T>(string json)
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
return JsonConvert.DeserializeObject<T>(json);
}
public virtual string ToString<T>(T obj)
{
return JsonConvert.SerializeObject(obj);
}
}
}

@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer
@ -18,14 +16,29 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/tx/{0}" : "https://blockstream.info/testnet/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
}
});
}
}

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -27,8 +27,7 @@ namespace BTCPayServer
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
: new KeyPath("1'"),
MinFee = Money.Satoshis(1m)
: new KeyPath("1'")
});
}
}

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
@ -28,8 +27,7 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
MinFee = Money.Coins(1m)
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}
}

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -10,20 +10,21 @@ namespace BTCPayServer
{
public void InitGroestlcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Groestlcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "groestlcoin",
DefaultRateRules = new[]
{
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
@ -17,14 +16,30 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Litecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://live.blockcypher.com/ltc/tx/{0}/"
: "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy },
{0x049d7cb2U, DerivationType.SegwitP2SH },
{0x04b24746U, DerivationType.Segwit },
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy },
{0x044a5262U, DerivationType.SegwitP2SH },
{0x045f1cf6U, DerivationType.Segwit }
}
});
}
}

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -3,17 +3,14 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
Dictionary<string, BTCPayNetworkBase> _Networks = new Dictionary<string, BTCPayNetworkBase>();
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
@ -25,13 +22,14 @@ namespace BTCPayServer
}
}
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
BTCPayNetworkProvider(BTCPayNetworkProvider unfiltered, string[] cryptoCodes)
{
NetworkType = filtered.NetworkType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
_Networks = new Dictionary<string, BTCPayNetwork>();
UnfilteredNetworks = unfiltered.UnfilteredNetworks ?? unfiltered;
NetworkType = unfiltered.NetworkType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(unfiltered.NetworkType);
_Networks = new Dictionary<string, BTCPayNetworkBase>();
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
foreach (var network in filtered._Networks)
foreach (var network in unfiltered._Networks)
{
if(cryptoCodes.Contains(network.Key))
{
@ -40,9 +38,12 @@ namespace BTCPayServer
}
}
public BTCPayNetworkProvider UnfilteredNetworks { get; }
public NetworkType NetworkType { get; private set; }
public BTCPayNetworkProvider(NetworkType networkType)
{
UnfilteredNetworks = this;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;
InitBitcoin();
@ -56,6 +57,22 @@ namespace BTCPayServer
InitGroestlcoin();
InitViacoin();
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
{
if(network.ElectrumMapping.Count == 0)
{
network.ElectrumMapping = GetNetwork<BTCPayNetwork>("BTC").ElectrumMapping;
if (!network.NBitcoinNetwork.Consensus.SupportSegwit)
{
network.ElectrumMapping =
network.ElectrumMapping
.Where(kv => kv.Value == DerivationType.Legacy)
.ToDictionary(k => k.Key, k => k.Value);
}
}
}
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
@ -73,20 +90,14 @@ namespace BTCPayServer
}
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC
{
get
{
return GetNetwork("BTC");
}
}
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
public void Add(BTCPayNetwork network)
public void Add(BTCPayNetworkBase network)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
public IEnumerable<BTCPayNetwork> GetAll()
public IEnumerable<BTCPayNetworkBase> GetAll()
{
return _Networks.Values.ToArray();
}
@ -95,15 +106,20 @@ namespace BTCPayServer
{
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
}
public BTCPayNetwork GetNetwork(string cryptoCode)
public BTCPayNetworkBase GetNetwork(string cryptoCode)
{
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
return GetNetwork<BTCPayNetworkBase>(cryptoCode);
}
public T GetNetwork<T>(string cryptoCode) where T: BTCPayNetworkBase
{
if (cryptoCode == null)
throw new ArgumentNullException(nameof(cryptoCode));
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetworkBase network))
{
if (cryptoCode == "XBT")
return GetNetwork("BTC");
return GetNetwork<T>("BTC");
}
return network;
return network as T;
}
}
}

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="NBitcoin" Version="4.1.2.38" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.18" />
</ItemGroup>
</Project>

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace BTCPayServer
{
class CustomThreadPool : IDisposable
public class CustomThreadPool : IDisposable
{
CancellationTokenSource _Cancel = new CancellationTokenSource();
TaskCompletionSource<bool> _Exited;

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer
{
public static class UtilitiesExtensions
{
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
{
foreach (var item in items)
{
hashSet.Add(item);
}
}
}
}

@ -6,7 +6,7 @@ using System.Text;
namespace BTCPayServer
{
class ZipUtils
public class ZipUtils
{
public static byte[] Zip(string unzipped)
{

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Folder Include="Providers\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
</ItemGroup>
</Project>

@ -53,7 +53,7 @@ namespace BTCPayServer.Rating
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = currencyPair.Substring(0, i);
var network = _NetworkProvider.GetNetwork(potentialCryptoName);
var network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(potentialCryptoName);
if (network != null)
{
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));

@ -4,9 +4,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Rating;
using System.Threading;
using Microsoft.Extensions.Logging.Abstractions;
using BTCPayServer.Logging;
namespace BTCPayServer.Services.Rates
{
@ -16,6 +17,7 @@ namespace BTCPayServer.Services.Rates
{
public ExchangeRates Latest;
public DateTimeOffset NextRefresh;
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
@ -37,6 +39,7 @@ namespace BTCPayServer.Services.Rates
}
IRateProvider _Inner;
public BackgroundFetcherRateProvider(IRateProvider inner)
{
if (inner == null)
@ -90,14 +93,15 @@ namespace BTCPayServer.Services.Rates
}
public bool DoNotAutoFetchIfExpired { get; set; }
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
public async Task<LatestFetch> UpdateIfNecessary()
public async Task<LatestFetch> UpdateIfNecessary(CancellationToken cancellationToken)
{
if (NextUpdate <= DateTimeOffset.UtcNow)
{
try
{
await Fetch();
await Fetch(cancellationToken);
}
catch { } // Exception is inside _Latest
return _Latest;
@ -106,7 +110,7 @@ namespace BTCPayServer.Services.Rates
}
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var latest = _Latest;
if (!DoNotAutoFetchIfExpired && latest != null && latest.Expiration <= DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1.0))
@ -114,7 +118,7 @@ namespace BTCPayServer.Services.Rates
Logs.PayServer.LogWarning($"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this.");
latest = null;
}
return (latest ?? (await Fetch())).GetResult();
return (latest ?? (await Fetch(cancellationToken))).GetResult();
}
private string GetExchangeName()
@ -124,14 +128,14 @@ namespace BTCPayServer.Services.Rates
return "???";
}
private async Task<LatestFetch> Fetch()
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken)
{
var previous = _Latest;
var fetch = new LatestFetch();
fetch.ExchangeName = GetExchangeName();
try
{
var rates = await _Inner.GetRatesAsync();
var rates = await _Inner.GetRatesAsync(cancellationToken);
fetch.Latest = rates;
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
fetch.NextRefresh = DateTimeOffset.UtcNow + RefreshRate;
@ -142,12 +146,15 @@ namespace BTCPayServer.Services.Rates
{
fetch.Latest = previous.Latest;
fetch.Expiration = previous.Expiration;
fetch.Backoff = previous.Backoff * 2;
if (fetch.Backoff > MaxBackoff)
fetch.Backoff = MaxBackoff;
}
else
{
fetch.Expiration = DateTimeOffset.UtcNow;
}
fetch.NextRefresh = DateTimeOffset.UtcNow;
fetch.NextRefresh = DateTimeOffset.UtcNow + fetch.Backoff;
fetch.Exception = ex;
}
_Latest = fetch;

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

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

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class ByllsRateProvider : IRateProvider, IHasExchangeName
{
private readonly HttpClient _httpClient;
public ByllsRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => "bylls";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["public_price"]["to_price"].Value<decimal>();
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) });
}
}
}

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

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

@ -23,20 +23,18 @@ namespace BTCPayServer.Services.Rates
public class CoinAverageExchange
{
public CoinAverageExchange(string name, string display)
public CoinAverageExchange(string name, string display, string url)
{
Name = name;
Display = display;
Url = url;
}
public string Name { get; set; }
public string Display { get; set; }
public string Url
{
get
{
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
}
get;
set;
}
}
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
@ -47,7 +45,11 @@ namespace BTCPayServer.Services.Rates
public void Add(CoinAverageExchange exchange)
{
TryAdd(exchange.Name, exchange);
if (!TryAdd(exchange.Name, exchange))
{
this.Remove(exchange.Name);
this.Add(exchange.Name, exchange);
}
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
@ -68,62 +70,73 @@ namespace BTCPayServer.Services.Rates
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new CoinAverageExchanges();
foreach(var item in
foreach (var item in
new[] {
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "EtherDelta", Name: "etherdelta"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "GDAX", Name: "gdax"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "Gemini", Name: "gemini"),
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "itBit", Name: "itbit"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "Foxbit", Name: "foxbit"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Liqui", Name: "liqui"),
(DisplayName: "BitKonan", Name: "bitkonan"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "Coinsecure", Name: "coinsecure"),
(DisplayName: "Idex", Name: "idex"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "CampBX", Name: "campbx"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "IDAX", Name: "idax"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "CEX.IO", Name: "cex"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "Stex", Name: "stex"),
(DisplayName: "CoinTiger", Name: "cointiger"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "Tokenomy", Name: "tokenomy"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Kryptono", Name: "kryptono"),
(DisplayName: "Bitso", Name: "bitso"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "Yobit", Name: "yobit"),
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Livecoin", Name: "livecoin"),
(DisplayName: "Hotbit", Name: "hotbit"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Coinbase Pro", Name: "coinbasepro"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Tokenize", Name: "tokenize"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Upbit", Name: "upbit"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Trade.io", Name: "tradeio"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Coinsquare", Name: "coinsquare"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Coinegg", Name: "coinegg"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "OasisDEX", Name: "oasisdex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "BitForex", Name: "bitforex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "itBit", Name: "itbit"),
})
{
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
}
// Keep back-compat
AvailableExchanges.Add(new CoinAverageExchange("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/coinbasepro"));
}
public Task AddHeader(HttpRequestMessage message)

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -31,14 +32,16 @@ namespace BTCPayServer.Services.Rates
public string ExchangeName => _ExchangeName;
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
await new SynchronizationContextRemover();
var rates = await _ExchangeAPI.GetTickersAsync();
lock (notFoundSymbols)
{
var exchangeRates =
rates.Select(t => CreateExchangeRate(t))
rates
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
.Select(t => CreateExchangeRate(t))
.Where(t => t != null)
.ToArray();
return new ExchangeRates(exchangeRates);

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

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

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -28,7 +29,7 @@ namespace BTCPayServer.Services.Rates
}
set
{
_LocalClient = null;
_LocalClient = value;
}
}
@ -38,7 +39,37 @@ namespace BTCPayServer.Services.Rates
static HttpClient _Client = new HttpClient();
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
{
{"ADAXBT","ADAXBT"},
{ "BSVUSD","BSVUSD"},
{ "QTUMEUR","QTUMEUR"},
{ "QTUMXBT","QTUMXBT"},
{ "EOSUSD","EOSUSD"},
{ "XTZUSD","XTZUSD"},
{ "XREPZUSD","XREPZUSD"},
{ "ADAEUR","ADAEUR"},
{ "ADAUSD","ADAUSD"},
{ "GNOEUR","GNOEUR"},
{ "XTZETH","XTZETH"},
{ "XXRPZJPY","XXRPZJPY"},
{ "XXRPZCAD","XXRPZCAD"},
{ "XTZEUR","XTZEUR"},
{ "QTUMETH","QTUMETH"},
{ "XXLMZUSD","XXLMZUSD"},
{ "QTUMCAD","QTUMCAD"},
{ "QTUMUSD","QTUMUSD"},
{ "XTZXBT","XTZXBT"},
{ "GNOUSD","GNOUSD"},
{ "ADAETH","ADAETH"},
{ "ADACAD","ADACAD"},
{ "XTZCAD","XTZCAD"},
{ "BSVEUR","BSVEUR"},
{ "XZECZJPY","XZECZJPY"},
{ "XXLMZEUR","XXLMZEUR"},
{"EOSEUR","EOSEUR"},
{"BSVXBT","BSVXBT"}
});
string[] _Symbols = Array.Empty<string>();
DateTimeOffset? _LastSymbolUpdate = null;
@ -55,7 +86,7 @@ namespace BTCPayServer.Services.Rates
{ "ZCAD", "CAD" },
};
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var result = new ExchangeRates();
var symbols = await GetSymbolsAsync();
@ -135,7 +166,7 @@ namespace BTCPayServer.Services.Rates
}
}
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null)
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null, CancellationToken cancellationToken = default)
{
StringBuilder sb = new StringBuilder();
sb.Append("https://api.kraken.com");
@ -147,7 +178,7 @@ namespace BTCPayServer.Services.Rates
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
}
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
var response = await HttpClient.SendAsync(request);
var response = await HttpClient.SendAsync(request, cancellationToken);
string stringResult = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<T>(stringResult);
if (result is JToken json)

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

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

@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
@ -21,12 +23,12 @@ namespace BTCPayServer.Services.Rates
{
_inner = inner;
}
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
{
return await _inner.GetRatesAsync();
return await _inner.GetRatesAsync(cancellationToken);
}
catch (Exception ex)
{
@ -108,12 +110,12 @@ namespace BTCPayServer.Services.Rates
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
@ -166,19 +168,20 @@ namespace BTCPayServer.Services.Rates
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
return exchanges;
}
public async Task<QueryRateResult> QueryRates(string exchangeName)
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
{
Providers.TryGetValue(exchangeName, out var directProvider);
directProvider = directProvider ?? NullRateProvider.Instance;
var wrapper = new WrapperRateProvider(directProvider);
var value = await wrapper.GetRatesAsync();
var value = await wrapper.GetRatesAsync(cancellationToken);
return new QueryRateResult()
{
Latency = wrapper.Latency,

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Tests.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Xunit;
using Xunit.Abstractions;
using System.Net.Http;
using System.Net.Http.Headers;
using BTCPayServer.Data;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenQA.Selenium;
namespace BTCPayServer.Tests
{
public class AuthenticationTests
{
public AuthenticationTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task GetRedirectedToLoginPathOnChallenge()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var client = tester.PayTester.HttpClient;
//Wallets endpoint is protected
var response = await client.GetAsync("wallets");
var urlPath = response.RequestMessage.RequestUri.ToString()
.Replace(tester.PayTester.ServerUri.ToString(), "");
//Cookie Challenge redirects you to login page
Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase);
var queryString = response.RequestMessage.RequestUri.ParseQueryString();
Assert.NotNull(queryString["ReturnUrl"]);
Assert.Equal("/wallets", queryString["ReturnUrl"]);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanGetOpenIdConfiguration()
{
using (var tester = ServerTester.Create())
{
tester.Start();
using (var response =
await tester.PayTester.HttpClient.GetAsync("/.well-known/openid-configuration"))
{
using (var streamToReadFrom = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
var json = await streamToReadFrom.ReadToEndAsync();
Assert.NotNull(json);
var configuration = OpenIdConnectConfiguration.Create(json);
Assert.NotNull(configuration);
}
}
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseNonInteractiveFlows()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var token = await RegisterPasswordClientAndGetAccessToken(user, null, tester);
await TestApiAgainstAccessToken(token, tester, user);
token = await RegisterPasswordClientAndGetAccessToken(user, "secret", tester);
await TestApiAgainstAccessToken(token, tester, user);
token = await RegisterClientCredentialsFlowAndGetAccessToken(user, "secret", tester);
await TestApiAgainstAccessToken(token, tester, user);
}
}
[Trait("Selenium", "Selenium")]
[Fact]
public async Task CanUseImplicitFlow()
{
using (var s = SeleniumTester.Create())
{
s.Start();
var tester = s.Server;
var user = tester.NewAccount();
user.GrantAccess();
var id = Guid.NewGuid().ToString();
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Implicit},
RedirectUris = {redirecturi}
});
var implicitAuthorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid&nonce={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
var url = s.Driver.Url;
var results = url.Split("#").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await TestApiAgainstAccessToken(results["access_token"], tester, user);
//in Implicit mode, you renew your token by hitting the same endpoint but adding prompt=none. If you are still logged in on the site, you will receive a fresh token.
var implicitAuthorizeUrlSilentModel = new Uri($"{implicitAuthorizeUrl.OriginalString}&prompt=none");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
url = s.Driver.Url;
results = url.Split("#").Last().Split("&").ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await TestApiAgainstAccessToken(results["access_token"], tester, user);
LogoutFlow(tester, id, s);
}
}
void LogoutFlow(ServerTester tester, string clientId, SeleniumTester seleniumTester)
{
var logoutUrl = new Uri(tester.PayTester.ServerUri,
$"connect/logout?response_type=token&client_id={clientId}");
seleniumTester.Driver.Navigate().GoToUrl(logoutUrl);
seleniumTester.GoToHome();
Assert.Throws<NoSuchElementException>(() => seleniumTester.Driver.FindElement(By.Id("Logout")));
}
[Trait("Selenium", "Selenium")]
[Fact]
public async Task CanUseCodeFlow()
{
using (var s = SeleniumTester.Create())
{
s.Start();
var tester = s.Server;
var user = tester.NewAccount();
user.GrantAccess();
var id = Guid.NewGuid().ToString();
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
var secret = "secret";
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions =
{
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken
},
RedirectUris = {redirecturi}
}, secret);
var authorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=code&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid offline_access&state={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(authorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
var url = s.Driver.Url;
var results = url.Split("?").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.AuthorizationCode),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret),
new KeyValuePair<string, string>("code", results["code"]),
new KeyValuePair<string, string>("redirect_uri", redirecturi.AbsoluteUri)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
await TestApiAgainstAccessToken(result.AccessToken, tester, user);
var refreshedAccessToken = await RefreshAnAccessToken(result.RefreshToken, httpClient, id, secret);
await TestApiAgainstAccessToken(refreshedAccessToken, tester, user);
}
}
private static async Task<string> RefreshAnAccessToken(string refreshToken, HttpClient client, string clientId,
string clientSecret = null)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(client.BaseAddress, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.RefreshToken),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("refresh_token", refreshToken)
})
};
var response = await client.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
private static async Task<string> RegisterClientCredentialsFlowAndGetAccessToken(TestAccount user,
string secret,
ServerTester tester)
{
var id = Guid.NewGuid().ToString();
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.ClientCredentials}
}, secret);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.ClientCredentials),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
private static async Task<string> RegisterPasswordClientAndGetAccessToken(TestAccount user, string secret,
ServerTester tester)
{
var id = Guid.NewGuid().ToString();
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Password}
}, secret);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type", OpenIddictConstants.GrantTypes.Password),
new KeyValuePair<string, string>("username", user.RegisterDetails.Email),
new KeyValuePair<string, string>("password", user.RegisterDetails.Password),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount)
{
var resultUser =
await TestApiAgainstAccessToken<string>(accessToken, "api/test/me/id",
tester.PayTester.HttpClient);
Assert.Equal(testAccount.UserId, resultUser);
var secondUser = tester.NewAccount();
secondUser.GrantAccess();
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, "api/test/me/stores",
tester.PayTester.HttpClient);
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"api/test/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient));
Assert.Equal(testAccount.RegisterDetails.IsAdmin, await TestApiAgainstAccessToken<bool>(accessToken,
$"api/test/me/is-admin",
tester.PayTester.HttpClient));
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken, $"api/test/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
public async Task<T> TestApiAgainstAccessToken<T>(string accessToken, string url, HttpClient client)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
new Uri(client.BaseAddress, url));
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var result = await client.SendAsync(httpRequest);
result.EnsureSuccessStatusCode();
var rawJson = await result.Content.ReadAsStringAsync();
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(rawJson, typeof(T));
}
return JsonConvert.DeserializeObject<T>(rawJson);
}
}
}

@ -10,7 +10,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="74.0.3729.6" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
@ -31,6 +33,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

@ -1,4 +1,4 @@
using BTCPayServer.Configuration;
using BTCPayServer.Configuration;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
@ -33,8 +33,12 @@ using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using AspNet.Security.OpenIdConnect.Primitives;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
@ -101,15 +105,18 @@ namespace BTCPayServer.Tests
StringBuilder config = new StringBuilder();
config.AppendLine($"{chain.ToLowerInvariant()}=1");
if (InContainer)
{
config.AppendLine($"bind=0.0.0.0");
}
config.AppendLine($"port={Port}");
config.AppendLine($"chains=btc,ltc");
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"btc.explorer.cookiefile=0");
config.AppendLine("allow-admin-registration=1");
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
@ -120,15 +127,18 @@ namespace BTCPayServer.Tests
File.WriteAllText(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.UseContentRoot(FindBTCPayServerDirectory())
.ConfigureServices(s =>
{
s.AddLogging(l =>
{
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
@ -138,14 +148,18 @@ namespace BTCPayServer.Tests
.UseKestrel()
.UseStartup<Startup>()
.Build();
_Host.Start();
_Host.StartWithTasksAsync().GetAwaiter().GetResult();
var urls = _Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
foreach (var url in urls)
{
Logs.Tester.LogInformation("Listening on " + url);
}
Logs.Tester.LogInformation("Server URI " + ServerUri);
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
while(!dashBoard.IsFullySynched())
{
Thread.Sleep(10);
}
Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));
if (MockRates)
{
@ -206,8 +220,45 @@ namespace BTCPayServer.Tests
});
rateProvider.Providers.Add("bittrex", bittrex);
}
WaitSiteIsOperational().GetAwaiter().GetResult();
}
private async Task WaitSiteIsOperational()
{
var synching = WaitIsFullySynched();
var accessingHomepage = WaitCanAccessHomepage();
await Task.WhenAll(synching, accessingHomepage).ConfigureAwait(false);
}
private async Task WaitCanAccessHomepage()
{
var resp = await HttpClient.GetAsync("/").ConfigureAwait(false);
while (resp.StatusCode != HttpStatusCode.OK)
{
await Task.Delay(10).ConfigureAwait(false);
}
}
private async Task WaitIsFullySynched()
{
var dashBoard = GetService<NBXplorerDashboard>();
while (!dashBoard.IsFullySynched())
{
await Task.Delay(10).ConfigureAwait(false);
}
}
private string FindBTCPayServerDirectory()
{
var solutionDirectory = LanguageService.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
}
public HttpClient HttpClient { get; set; }
public string HostName
{
get;
@ -215,6 +266,7 @@ namespace BTCPayServer.Tests
}
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public BTCPayNetworkProvider Networks { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
@ -223,6 +275,8 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>();
}
public IServiceProvider ServiceProvider => _Host.Services;
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
{
var context = new DefaultHttpContext();
@ -232,7 +286,7 @@ namespace BTCPayServer.Tests
if (userId != null)
{
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
claims.Add(new Claim(OpenIdConnectConstants.Claims.Subject, userId));
if (additionalClaims != null)
claims.AddRange(additionalClaims);
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));

@ -27,7 +27,7 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Fact(Timeout = 60000)]
[Trait("Integration", "Integration")]
public async void CanSetChangellyPaymentMethod()
{
@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
.Value);
}
}

@ -5,10 +5,8 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Crowdfund;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Hubs;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
@ -25,6 +23,7 @@ using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.UnitTest1;
namespace BTCPayServer.Tests
{
@ -110,7 +109,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
}, default));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
@ -119,7 +118,7 @@ namespace BTCPayServer.Tests
{
RedirectToCheckout = false,
Amount = new decimal(0.01)
}));
}, default));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
@ -131,7 +130,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
}, default));
//Scenario 4: Enabled But End Date < Now - Not Allowed
@ -143,7 +142,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
}, default));
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
@ -157,13 +156,13 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
}));
}, default));
//Scenario 6: Allowed
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.05)
}));
}, default));
}
}
@ -178,6 +177,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
@ -185,7 +185,8 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
Logs.Tester.LogInformation("We create an invoice with a hardcap");
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.Enabled = true;
@ -193,6 +194,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetAmount = 100;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
@ -209,16 +211,16 @@ namespace BTCPayServer.Tests
Assert.Equal(0m, model.Info.CurrentAmount );
Assert.Equal(0m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
@ -228,19 +230,69 @@ namespace BTCPayServer.Tests
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress,invoice.BtcDue);
Assert.Equal(0m ,model.Info.CurrentAmount );
Assert.Equal(0m ,model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount);
Assert.Equal( 0m, model.Info.ProgressPercentage);
Assert.Equal(0m, model.Info.ProgressPercentage);
Assert.Equal(1m, model.Info.PendingProgressPercentage);
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(1m, model.Info.CurrentAmount);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
});
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m));
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}

@ -1,17 +1,28 @@
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
RUN apk add --no-cache curl
WORKDIR /source
COPY Build/Common.csproj Build/Common.csproj
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
COPY BTCPayServer.Common/BTCPayServer.Common.csproj BTCPayServer.Common/BTCPayServer.Common.csproj
COPY BTCPayServer.Rating/BTCPayServer.Rating.csproj BTCPayServer.Rating/BTCPayServer.Rating.csproj
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
RUN apk add --no-cache chromium chromium-chromedriver icu-libs
ENV SCREEN_HEIGHT 600 \
SCREEN_WIDTH 1200
COPY . .
RUN dotnet build
RUN cd BTCPayServer.Tests && dotnet build
WORKDIR /source/BTCPayServer.Tests
ENTRYPOINT ["./docker-entrypoint.sh"]

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using OpenQA.Selenium;
using Xunit;
namespace BTCPayServer.Tests
{
public static class Extensions
{
public static void ScrollTo(this IWebDriver driver, By by)
{
var element = driver.FindElement(by);
((IJavaScriptExecutor)driver).ExecuteScript($"window.scrollBy({element.Location.X},{element.Location.Y});");
}
/// <summary>
/// Sometimes the chrome driver is fucked up and we need some magic to click on the element.
/// </summary>
/// <param name="element"></param>
public static void ForceClick(this IWebElement element)
{
element.SendKeys(Keys.Return);
}
public static void AssertNoError(this IWebDriver driver)
{
try
{
Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand")));
}
catch
{
StringBuilder builder = new StringBuilder();
builder.AppendLine();
foreach (var logKind in new []{ LogType.Browser, LogType.Client, LogType.Driver, LogType.Server })
{
try
{
var logs = driver.Manage().Logs.GetLog(logKind);
builder.AppendLine($"Selenium [{logKind}]:");
foreach (var entry in logs)
{
builder.AppendLine($"[{entry.Level}]: {entry.Message}");
}
}
catch { }
builder.AppendLine($"---------");
}
Logs.Tester.LogInformation(builder.ToString());
builder = new StringBuilder();
builder.AppendLine($"Selenium [Sources]:");
builder.AppendLine(driver.PageSource);
builder.AppendLine($"---------");
Logs.Tester.LogInformation(builder.ToString());
throw;
}
}
public static T AssertViewModel<T>(this IActionResult result)
{
Assert.NotNull(result);
var vr = Assert.IsType<ViewResult>(result);
return Assert.IsType<T>(vr.Model);
}
public static async Task<T> AssertViewModelAsync<T>(this Task<IActionResult> task)
{
var result = await task;
Assert.NotNull(result);
var vr = Assert.IsType<ViewResult>(result);
return Assert.IsType<T>(vr.Model);
}
}
}

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

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

@ -0,0 +1,126 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class PSBTTests
{
public PSBTTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanPlayWithPSBT()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
var walletId = new WalletId(user.StoreId, "BTC");
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
var sendModel = new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
DestinationAddress = sendDestination,
Amount = 0.1m,
}
},
FeeSatoshiPerByte = 1,
CurrentBalance = 1.5m
};
var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmLedger.SuccessPath);
Assert.NotNull(vmLedger.WebsocketPath);
var redirectedPSBT = (string)Assert.IsType<RedirectToActionResult>(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"];
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmPSBT.Decoded);
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.ExtKey);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
Assert.Contains(psbtReady.Destinations, d => d.Positive);
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
vmPSBT.PSBT = unsignedPSBT.ToBase64();
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
combineVM.PSBT = signedPSBT.ToBase64();
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
// Can use uploaded file?
combineVM.PSBT = null;
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
}
}
}
}

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

@ -41,10 +41,15 @@ You can call bitcoin-cli inside the container with `docker exec`, for example, i
```
If you are using Powershell:
```
```powershell
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
You can also generate blocks:
```powershell
.\docker-bitcoin-generate.ps1 3
```
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.

@ -34,7 +34,7 @@ namespace BTCPayServer.Tests
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
builder.AppendLine("// Some other cool comments");
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
builder.AppendLine("BTC_usd = kraken(BTC_USD)");
builder.AppendLine("BTC_X = Coinbase(BTC_X);");
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
@ -45,14 +45,14 @@ namespace BTCPayServer.Tests
"DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" +
"DOGE_BTC = bittrex(DOGE_BTC);\n" +
"// Some other cool comments\n" +
"BTC_USD = gdax(BTC_USD);\n" +
"BTC_USD = kraken(BTC_USD);\n" +
"BTC_X = coinbase(BTC_X);\n" +
"X_X = coinaverage(X_X) * 1.02;",
rules.ToString());
var tests = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * kraken(BTC_USD) * 1.1"),
(Pair: "BTC_USD", Expected: "kraken(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
@ -62,14 +62,14 @@ namespace BTCPayServer.Tests
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
rules.Spread = 0.2m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
Assert.Equal("(bittrex(DOGE_BTC) * kraken(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
////////////////
// Check errors conditions
builder = new StringBuilder();
builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1");
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
builder.AppendLine("BTC_usd = kraken(BTC_USD)");
builder.AppendLine("LTC_CHF = LTC_CHF * 1.01");
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
@ -77,7 +77,7 @@ namespace BTCPayServer.Tests
tests = new[]
{
(Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"),
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"),
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * kraken(BTC_USD) * 1.1"),
(Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"),
};
foreach (var test in tests)
@ -90,15 +90,15 @@ namespace BTCPayServer.Tests
builder = new StringBuilder();
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
builder.AppendLine("BTC_usd = kraken(BTC_USD)");
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
var tests2 = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * kraken(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),kraken(BTC_USD)"),
(Pair: "BTC_USD", Expected: "kraken(BTC_USD)", ExpectedExchangeRates: "kraken(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),

@ -0,0 +1,151 @@
using System;
using BTCPayServer;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using Xunit;
using System.IO;
using BTCPayServer.Tests.Logging;
using System.Threading;
namespace BTCPayServer.Tests
{
public class SeleniumTester : IDisposable
{
public IWebDriver Driver { get; set; }
public ServerTester Server { get; set; }
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null)
{
var server = ServerTester.Create(scope);
return new SeleniumTester()
{
Server = server
};
}
public void Start()
{
Server.Start();
ChromeOptions options = new ChromeOptions();
options.AddArguments("headless"); // Comment to view browser
options.AddArguments("window-size=1200x600"); // Comment to view browser
options.AddArgument("shm-size=2g");
if (Server.PayTester.InContainer)
{
options.AddArgument("no-sandbox");
}
Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options);
Logs.Tester.LogInformation("Selenium: Using chrome driver");
Logs.Tester.LogInformation("Selenium: Browsing to " + Server.PayTester.ServerUri);
Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}");
Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
Driver.Navigate().GoToUrl(Server.PayTester.ServerUri);
Driver.AssertNoError();
}
public string Link(string relativeLink)
{
return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash();
}
public string RegisterNewUser(bool isAdmin = false)
{
var usr = RandomUtils.GetUInt256().ToString() + "@a.com";
Driver.FindElement(By.Id("Register")).Click();
Driver.FindElement(By.Id("Email")).SendKeys(usr);
Driver.FindElement(By.Id("Password")).SendKeys("123456");
Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
if (isAdmin)
Driver.FindElement(By.Id("IsAdmin")).Click();
Driver.FindElement(By.Id("RegisterButton")).Click();
Driver.AssertNoError();
return usr;
}
public string CreateNewStore()
{
var usr = "Store" + RandomUtils.GetUInt64().ToString();
Driver.FindElement(By.Id("Stores")).Click();
Driver.FindElement(By.Id("CreateStore")).Click();
Driver.FindElement(By.Id("Name")).SendKeys(usr);
Driver.FindElement(By.Id("Create")).Click();
return usr;
}
public void AddDerivationScheme(string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
Driver.FindElement(By.Id("ModifyBTC")).ForceClick();
Driver.FindElement(By.ClassName("store-derivation-scheme")).SendKeys(derivationScheme);
Driver.FindElement(By.Id("Continue")).ForceClick();
Driver.FindElement(By.Id("Confirm")).ForceClick();
Driver.FindElement(By.Id("Save")).ForceClick();
return;
}
public void ClickOnAllSideMenus()
{
var links = Driver.FindElements(By.CssSelector(".nav-pills .nav-link")).Select(c => c.GetAttribute("href")).ToList();
Driver.AssertNoError();
Assert.NotEmpty(links);
foreach (var l in links)
{
Driver.Navigate().GoToUrl(l);
Driver.AssertNoError();
}
}
public void CreateInvoice(string random)
{
Driver.FindElement(By.Id("Invoices")).Click();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100");
Driver.FindElement(By.Name("StoreId")).SendKeys("Deriv" + random + Keys.Enter);
Driver.FindElement(By.Id("Create")).Click();
return;
}
public void Dispose()
{
if (Driver != null)
{
try
{
Driver.Close();
}
catch { }
Driver.Dispose();
}
if (Server != null)
Server.Dispose();
}
internal void AssertNotFound()
{
Assert.Contains("Status Code: 404; Not Found", Driver.PageSource);
}
public void GoToHome()
{
Driver.Navigate().GoToUrl(Server.PayTester.ServerUri);
}
public void Logout()
{
Driver.FindElement(By.Id("Logout")).Click();
}
public void Login(string user, string password)
{
Driver.FindElement(By.Id("Email")).SendKeys(user);
Driver.FindElement(By.Id("Password")).SendKeys(password);
Driver.FindElement(By.Id("LoginButton")).Click();
}
}
}

@ -0,0 +1,372 @@
using System;
using Xunit;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using BTCPayServer.Tests.Logging;
using Xunit.Abstractions;
using OpenQA.Selenium.Interactions;
using System.Linq;
using NBitcoin;
namespace BTCPayServer.Tests
{
[Trait("Selenium", "Selenium")]
public class ChromeTests
{
public ChromeTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanNavigateServerSettings()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser(true);
s.Driver.FindElement(By.Id("ServerSettings")).Click();
s.Driver.AssertNoError();
s.ClickOnAllSideMenus();
s.Driver.Quit();
}
}
[Fact]
public void NewUserLogin()
{
using (var s = SeleniumTester.Create())
{
s.Start();
//Register & Log Out
var email = s.RegisterNewUser();
s.Driver.FindElement(By.Id("Logout")).Click();
s.Driver.AssertNoError();
s.Driver.FindElement(By.Id("Login")).Click();
s.Driver.AssertNoError();
s.Driver.Navigate().GoToUrl(s.Link("/invoices"));
Assert.Contains("ReturnUrl=%2Finvoices", s.Driver.Url);
// We should be redirected to login
//Same User Can Log Back In
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
// We should be redirected to invoice
Assert.EndsWith("/invoices", s.Driver.Url);
// Should not be able to reach server settings
s.Driver.Navigate().GoToUrl(s.Link("/server/users"));
Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url);
//Change Password & Log Out
s.Driver.FindElement(By.Id("MySettings")).Click();
s.Driver.FindElement(By.Id("ChangePassword")).Click();
s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
s.Driver.FindElement(By.Id("UpdatePassword")).Click();
s.Driver.FindElement(By.Id("Logout")).Click();
s.Driver.AssertNoError();
//Log In With New Password
s.Driver.FindElement(By.Id("Login")).Click();
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
s.Driver.FindElement(By.Id("LoginButton")).Click();
Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores");
s.Driver.FindElement(By.Id("MySettings")).Click();
s.ClickOnAllSideMenus();
s.Driver.Quit();
}
}
static void LogIn(SeleniumTester s, string email)
{
s.Driver.FindElement(By.Id("Login")).Click();
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
s.Driver.AssertNoError();
}
[Fact]
public void CanUseDynamicDns()
{
using (var s = SeleniumTester.Create())
{
s.Start();
var alice = s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
Assert.Contains("Dynamic DNS", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns"));
s.Driver.AssertNoError();
if (s.Driver.PageSource.Contains("pouet.hello.com"))
{
// Cleanup old test run
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
s.Driver.FindElement(By.Id("continue")).Click();
}
s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
s.Driver.AssertNoError();
// We will just cheat for test purposes by only querying the server
s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/"));
s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com");
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog");
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter);
s.Driver.AssertNoError();
Assert.Contains("The Dynamic DNS has been successfully queried", s.Driver.PageSource);
Assert.EndsWith("/server/services/dynamic-dns", s.Driver.Url);
// Try to do the same thing should fail (hostname already exists)
s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
s.Driver.AssertNoError();
s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/"));
s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com");
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog");
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter);
s.Driver.AssertNoError();
Assert.Contains("This hostname already exists", s.Driver.PageSource);
// Delete it
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns"));
Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.AssertNoError();
Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
}
}
[Fact]
public void CanCreateStores()
{
using (var s = SeleniumTester.Create())
{
s.Start();
var alice = s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.AssertNoError();
Assert.Contains(store, s.Driver.PageSource);
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
CreateInvoice(s, store);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
var invoiceUrl = s.Driver.Url;
// When logout we should not be able to access store and invoice details
s.Driver.FindElement(By.Id("Logout")).Click();
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
// When logged we should not be able to access store and invoice details
var bob = s.RegisterNewUser();
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl);
s.AssertNotFound();
s.GoToHome();
s.Logout();
// Let's add Bob as a guest to alice's store
LogIn(s, alice);
s.Driver.Navigate().GoToUrl(storeUrl + "/users");
s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter);
Assert.Contains("User added successfully", s.Driver.PageSource);
s.Logout();
// Bob should not have access to store, but should have access to invoice
LogIn(s, bob);
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl);
s.Driver.AssertNoError();
}
}
[Fact]
public void CanCreateInvoice()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme();
CreateInvoice(s, store);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
s.Driver.AssertNoError();
s.Driver.Navigate().Back();
s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl")));
s.Driver.Quit();
}
}
static void CreateInvoice(SeleniumTester s, string store)
{
s.Driver.FindElement(By.Id("Invoices")).Click();
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
s.Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100");
s.Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("Create")).Click();
Assert.True(s.Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
}
[Fact]
public void CanCreateAppPoS()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + store);
s.Driver.FindElement(By.CssSelector("select#SelectedAppType.form-control")).SendKeys("PointOfSale" + Keys.Enter);
s.Driver.FindElement(By.CssSelector("select#SelectedStore.form-control")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.CssSelector("input#EnableShoppingCart.form-check")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
Assert.True(s.Driver.PageSource.Contains("App updated"), "Unable to create PoS");
s.Driver.Quit();
}
}
[Fact]
public void CanCreateAppCF()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + store);
s.Driver.FindElement(By.CssSelector("select#SelectedAppType.form-control")).SendKeys("Crowdfund" + Keys.Enter);
s.Driver.FindElement(By.CssSelector("select#SelectedStore.form-control")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
s.Driver.FindElement(By.Id("SaveSettings")).Submit();
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.True(s.Driver.PageSource.Contains("Currently Active!"), "Unable to create CF");
s.Driver.Quit();
}
}
[Fact]
public void CanCreatePayRequest()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("SaveButton")).Submit();
s.Driver.FindElement(By.Name("ViewAppButton")).SendKeys(Keys.Return);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.True(s.Driver.PageSource.Contains("Amount due"), "Unable to create Payment Request");
s.Driver.Quit();
}
}
[Fact]
public void CanManageWallet()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
s.CreateNewStore();
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
// to sign the transaction
var mnemonic = "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage";
var root = new Mnemonic(mnemonic).DeriveExtKey();
s.AddDerivationScheme("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.ClickOnAllSideMenus();
// We setup the fingerprint and the account key path
s.Driver.FindElement(By.Id("WalletSettings")).ForceClick();
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick();
var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
void SignWith(string signingSource)
{
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(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);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
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);
var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString();
SignWith(accountKey);
}
}
}
}

@ -23,6 +23,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{
@ -43,13 +44,14 @@ namespace BTCPayServer.Tests
Directory.CreateDirectory(_Directory);
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork);
ExplorerNode.ScanRPCCapabilities();
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBitcoinNetwork);
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
var btc = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
@ -85,9 +87,11 @@ namespace BTCPayServer.Tests
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public Task EnsureChannelsSetup()
public async Task EnsureChannelsSetup()
{
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
Logs.Tester.LogInformation("Connecting channels");
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
Logs.Tester.LogInformation("Channels connected");
}
private IEnumerable<ILightningClient> GetLightningSenderClients()

@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Tests.Logging;
using DBriize.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class StorageTests
{
public StorageTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanConfigureStorage()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
//Once we select a provider, redirect to its view
var localResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.FileSystem
}));
Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName);
Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]);
var AmazonS3result = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AmazonS3
}));
Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName);
Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]);
var GoogleResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.GoogleCloudStorage
}));
Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName);
Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]);
var AzureResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AzureBlobStorage
}));
Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName);
Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]);
//Cool, we get redirected to the config pages
//Let's configure this stuff
//Let's try and cheat and go to an invalid storage provider config
Assert.Equal(nameof(Storage), (Assert
.IsType<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
.ActionName));
//ok no more messing around, let's configure this shit.
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
//local file system does not need config, easy days!
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
//ok cool, let's see if this got set right
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
//if we tell the settings page to force, it should allow us to select a new provider
Assert.IsType<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
//awesome, now let's see if the files result says we're all set up
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
Assert.True(viewFilesViewModel.StorageConfigured);
Assert.Empty(viewFilesViewModel.Files);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
await CanUploadRemoveFiles(controller);
}
}
[Fact]
[Trait("ExternalIntegration", "ExternalIntegration")]
public async Task CanUseAzureBlobStorage()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
var azureBlobStorageConfiguration = Assert.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model);
azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString");
azureBlobStorageConfiguration.ContainerName = "testscontainer";
Assert.IsType<ViewResult>(
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
var shouldBeRedirectingToAzureStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.AzureBlobStorage,
shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]);
//seems like azure config worked, let's see if the conn string was actually saved
Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert
.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(
await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model).ConnectionString);
await CanUploadRemoveFiles(controller);
}
}
private async Task CanUploadRemoveFiles(ServerController controller)
{
var fileContent = "content";
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent)));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
Assert.Equal("Files", uploadFormFileResult.ActionName);
//check if file was uploaded and saved in db
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.NotEmpty(viewFilesViewModel.Files);
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
//verify file is available and the same
var net = new System.Net.WebClient();
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
Assert.Equal(fileContent, data);
//create a temporary link to file
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
new ServerController.CreateTemporaryFileUrlViewModel()
{
IsDownload = true,
TimeAmount = 1,
TimeType = ServerController.CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes
}));
Assert.True(tmpLinkGenerate.RouteValues.ContainsKey("StatusMessage"));
var statusMessageModel = new StatusMessageModel(tmpLinkGenerate.RouteValues["StatusMessage"].ToString());
Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity);
var index = statusMessageModel.Html.IndexOf("target='_blank'>");
var url = statusMessageModel.Html.Substring(index).ReplaceMultiple(new Dictionary<string, string>()
{
{"</a>", string.Empty}, {"target='_blank'>", string.Empty}
});
//verify tmpfile is available and the same
data = await net.DownloadStringTaskAsync(new Uri(url));
Assert.Equal(fileContent, data);
//delete file
Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert
.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId))
.RouteValues["statusMessage"].ToString()).Severity);
//attempt to fetch deleted file
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.Null(viewFilesViewModel.DirectFileUrl);
Assert.Null(viewFilesViewModel.SelectedFileId);
}
private static string GetFromSecrets(string key)
{
var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}");
if (!string.IsNullOrEmpty(connStr) && connStr != "none")
return connStr;
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
var token = config[key];
Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} <value>\"");
return token;
}
}
}

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication.OpenId.Models;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
@ -18,6 +19,8 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
using OpenIddict.Abstractions;
using OpenIddict.Core;
namespace BTCPayServer.Tests
{
@ -95,7 +98,7 @@ namespace BTCPayServer.Tests
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
@ -113,15 +116,18 @@ namespace BTCPayServer.Tests
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel()
RegisterDetails = new RegisterViewModel()
{
Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
});
};
await account.Register(RegisterDetails);
UserId = account.RegisteredUserId;
}
public RegisterViewModel RegisterDetails{ get; set; }
public Bitpay BitPay
{
get; set;
@ -163,5 +169,14 @@ namespace BTCPayServer.Tests
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
public async Task<BTCPayOpenIdClient> RegisterOpenIdClient(OpenIddictApplicationDescriptor descriptor, string secret = null)
{
var openIddictApplicationManager = parent.PayTester.GetService<OpenIddictApplicationManager<BTCPayOpenIdClient>>();
var client = new BTCPayOpenIdClient {ApplicationUserId = UserId};
await openIddictApplicationManager.PopulateAsync(client, descriptor);
await openIddictApplicationManager.CreateAsync(client, secret);
return client;
}
}
}

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Xunit.Sdk;
namespace BTCPayServer.Tests
{
public static class TestUtils
{
public static FormFile GetFormFile(string filename, string content)
{
File.WriteAllText(filename, content);
var fileInfo = new FileInfo(filename);
FormFile formFile = new FormFile(
new FileStream(filename, FileMode.OpenOrCreate),
0,
fileInfo.Length, fileInfo.Name, fileInfo.Name)
{
Headers = new HeaderDictionary()
};
formFile.ContentType = "text/plain";
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
return formFile;
}
public static FormFile GetFormFile(string filename, byte[] content)
{
File.WriteAllBytes(filename, content);
var fileInfo = new FileInfo(filename);
FormFile formFile = new FormFile(
new FileStream(filename, FileMode.OpenOrCreate),
0,
fileInfo.Length, fileInfo.Name, fileInfo.Name)
{
Headers = new HeaderDictionary()
};
formFile.ContentType = "application/octet-stream";
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
return formFile;
}
public static void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
public static async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -9,6 +9,7 @@ using NBitcoin.DataEncoders;
using Newtonsoft.Json.Linq;
using Xunit;
using System.IO;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Tests
{

@ -0,0 +1,3 @@
$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 $args $address

@ -19,6 +19,8 @@ services:
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
@ -36,7 +38,7 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: btcpayserver/bitcoin:0.17.0
image: btcpayserver/bitcoin:0.18.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -53,7 +55,7 @@ services:
- merchant_lnd
devlnd:
image: btcpayserver/bitcoin:0.17.0
image: btcpayserver/bitcoin:0.18.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -65,11 +67,8 @@ services:
- mysql
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.8
image: nicolasdorier/nbxplorer:2.0.0.55
restart: unless-stopped
ports:
- "32838:32838"
@ -96,13 +95,14 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.17.0
image: btcpayserver/bitcoin:0.18.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
rpcbind=0.0.0.0:43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
@ -119,7 +119,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.6.2-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -131,6 +131,7 @@ services:
bind-addr=0.0.0.0
announce-addr=customer_lightningd
log-level=debug
funding-confirms=1
dev-broadcast-interval=1000
dev-bitcoind-poll=1
ports:
@ -165,7 +166,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.6.2-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -174,6 +175,7 @@ services:
bitcoin-rpcconnect=bitcoind
bind-addr=0.0.0.0
announce-addr=merchant_lightningd
funding-confirms=1
network=regtest
log-level=debug
dev-broadcast-interval=1000
@ -222,7 +224,7 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:v0.5.1-beta
image: btcpayserver/lnd:v0.7.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -237,6 +239,7 @@ services:
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
noseedbackup=1
@ -252,7 +255,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.5.1-beta
image: btcpayserver/lnd:v0.7.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -267,6 +270,7 @@ services:
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
bitcoin.defaultchanconfs=1
no-macaroons=1
debuglevel=debug
noseedbackup=1

@ -1,5 +1,9 @@
#!/bin/sh
set -e
dotnet test --filter Fast=Fast --no-build
dotnet test --filter Integration=Integration --no-build -v n
FILTERS=" "
if [[ "$TEST_FILTERS" ]]; then
FILTERS="--filter $TEST_FILTERS"
fi
dotnet test $FILTERS --no-build -v n

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

@ -0,0 +1,21 @@
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Authentication.OpenId
{
public class AuthorizationCodeGrantTypeEventHandler : OpenIdGrantHandlerCheckCanSignIn
{
public AuthorizationCodeGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions, userManager)
{
}
protected override bool IsValid(OpenIdConnectRequest request)
{
return request.IsAuthorizationCodeGrantType();
}
}
}

@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class AuthorizationEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleAuthorizationRequest>
{
private readonly UserManager<ApplicationUser> _userManager;
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleAuthorizationRequest notification)
{
if (!notification.Context.Request.IsAuthorizationRequest())
{
return OpenIddictServerEventState.Unhandled;
}
var auth = await notification.Context.HttpContext.AuthenticateAsync();
if (!auth.Succeeded)
{
// If the client application request promptless authentication,
// return an error indicating that the user is not logged in.
if (notification.Context.Request.HasPrompt(OpenIdConnectConstants.Prompts.None))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIdConnectConstants.Properties.Error] = OpenIdConnectConstants.Errors.LoginRequired,
[OpenIdConnectConstants.Properties.ErrorDescription] = "The user is not logged in."
});
// Ask OpenIddict to return a login_required error to the client application.
await notification.Context.HttpContext.ForbidAsync(properties);
notification.Context.HandleResponse();
return OpenIddictServerEventState.Handled;
}
await notification.Context.HttpContext.ChallengeAsync();
notification.Context.HandleResponse();
return OpenIddictServerEventState.Handled;
}
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(auth.Principal);
if (user == null)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "An internal error has occurred");
return OpenIddictServerEventState.Handled;
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(notification.Context.Request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
notification.Context.Validate(ticket);
return OpenIddictServerEventState.Handled;
}
public AuthorizationEventHandler(
UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions) : base(signInManager, identityOptions)
{
_userManager = userManager;
}
}
}

@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public abstract class BaseOpenIdGrantHandler<T> : IOpenIddictServerEventHandler<T>
where T : class, IOpenIddictServerEvent
{
protected readonly SignInManager<ApplicationUser> _signInManager;
protected readonly IOptions<IdentityOptions> _identityOptions;
protected BaseOpenIdGrantHandler(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions)
{
_signInManager = signInManager;
_identityOptions = identityOptions;
}
protected async Task<AuthenticationTicket> CreateTicketAsync(
OpenIdConnectRequest request, ApplicationUser user,
AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIddictServerDefaults.AuthenticationScheme);
if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType())
{
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
ticket.SetScopes(request.GetScopes());
}
foreach (var claim in ticket.Principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, ticket));
}
return ticket;
}
private IEnumerable<string> GetDestinations(Claim claim, AuthenticationTicket ticket)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case OpenIddictConstants.Claims.Name:
yield return OpenIddictConstants.Destinations.AccessToken;
if (ticket.HasScope(OpenIddictConstants.Scopes.Profile))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Email:
yield return OpenIddictConstants.Destinations.AccessToken;
if (ticket.HasScope(OpenIddictConstants.Scopes.Email))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Role:
yield return OpenIddictConstants.Destinations.AccessToken;
if (ticket.HasScope(OpenIddictConstants.Scopes.Roles))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
default:
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
yield break;
}
else
{
yield return OpenIddictConstants.Destinations.AccessToken;
yield break;
}
}
}
public abstract Task<OpenIddictServerEventState> HandleAsync(T notification);
}
}

@ -0,0 +1,61 @@
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Authentication.OpenId.Models;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class
ClientCredentialsGrantTypeEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequest>
{
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
private readonly UserManager<ApplicationUser> _userManager;
public ClientCredentialsGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions)
{
_applicationManager = applicationManager;
_userManager = userManager;
}
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleTokenRequest notification)
{
var request = notification.Context.Request;
if (!request.IsClientCredentialsGrantType())
{
// Allow other handlers to process the event.
return OpenIddictServerEventState.Unhandled;
}
var application = await _applicationManager.FindByClientIdAsync(request.ClientId,
notification.Context.HttpContext.RequestAborted);
if (application == null)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidClient,
description: "The client application was not found in the database.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
var user = await _userManager.FindByIdAsync(application.ApplicationUserId);
notification.Context.Validate(await CreateTicketAsync(request, user));
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class LogoutEventHandler: BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleLogoutRequest>
{
public LogoutEventHandler(SignInManager<ApplicationUser> signInManager, IOptions<IdentityOptions> identityOptions) : base(signInManager, identityOptions)
{
}
public override async Task<OpenIddictServerEventState> HandleAsync(OpenIddictServerEvents.HandleLogoutRequest notification)
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
await notification.Context.HttpContext.SignOutAsync(OpenIddictServerDefaults.AuthenticationScheme);
notification.Context.HandleResponse();
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,6 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Authentication.OpenId.Models
{
public class BTCPayOpenIdAuthorization : OpenIddictAuthorization<string, BTCPayOpenIdClient, BTCPayOpenIdToken> { }
}

@ -0,0 +1,11 @@
using BTCPayServer.Models;
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Authentication.OpenId.Models
{
public class BTCPayOpenIdClient: OpenIddictApplication<string, BTCPayOpenIdAuthorization, BTCPayOpenIdToken>
{
public string ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }
}
}

@ -0,0 +1,6 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Authentication.OpenId.Models
{
public class BTCPayOpenIdToken : OpenIddictToken<string, BTCPayOpenIdClient, BTCPayOpenIdAuthorization> { }
}

@ -0,0 +1,65 @@
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public abstract class
OpenIdGrantHandlerCheckCanSignIn : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequest>
{
private readonly UserManager<ApplicationUser> _userManager;
protected OpenIdGrantHandlerCheckCanSignIn(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions)
{
_userManager = userManager;
}
protected abstract bool IsValid(OpenIdConnectRequest request);
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleTokenRequest notification)
{
var request = notification.Context.Request;
if (!IsValid(request))
{
// Allow other handlers to process the event.
return OpenIddictServerEventState.Unhandled;
}
var scheme = notification.Context.Scheme.Name;
var authenticateResult = (await notification.Context.HttpContext.AuthenticateAsync(scheme));
var user = await _userManager.GetUserAsync(authenticateResult.Principal);
if (user == null)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The token is no longer valid.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The user is no longer allowed to sign in.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
notification.Context.Validate(await CreateTicketAsync(request, user));
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,57 @@
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using BTCPayServer.Services.U2F;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class PasswordGrantTypeEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequest>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly U2FService _u2FService;
public PasswordGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> identityOptions, U2FService u2FService) : base(signInManager, identityOptions)
{
_userManager = userManager;
_u2FService = u2FService;
}
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleTokenRequest notification)
{
var request = notification.Context.Request;
if (!request.IsPasswordGrantType())
{
// Allow other handlers to process the event.
return OpenIddictServerEventState.Unhandled;
}
// Validate the user credentials.
// Note: to mitigate brute force attacks, you SHOULD strongly consider
// applying a key derivation function like PBKDF2 to slow down
// the password validation process. You SHOULD also consider
// using a time-constant comparer to prevent timing attacks.
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null || await _u2FService.HasDevices(user.Id) ||
!(await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true))
.Succeeded)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The specified credentials are invalid.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
notification.Context.Validate(await CreateTicketAsync(request, user));
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,21 @@
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Authentication.OpenId
{
public class RefreshTokenGrantTypeEventHandler : OpenIdGrantHandlerCheckCanSignIn
{
public RefreshTokenGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions, userManager)
{
}
protected override bool IsValid(OpenIdConnectRequest request)
{
return request.IsRefreshTokenGrantType();
}
}
}

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

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

@ -1,12 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.53</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -30,36 +26,37 @@
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.5" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.23" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="DBriize" Version="1.0.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.7" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<PackageReference Include="OpenIddict" Version="2.0.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@ -67,7 +64,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.11.2" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
@ -123,9 +126,16 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="U2F\Services" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
</ItemGroup>
<ItemGroup>
@ -136,10 +146,19 @@
<Content Update="Views\Home\BitpayTranslator.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\DynamicDnsServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\P2PService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\DynamicDnsService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
@ -169,6 +188,15 @@
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBT.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

@ -6,16 +6,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using Serilog.Events;
namespace BTCPayServer.Configuration
@ -49,12 +42,8 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public EndPoint SocksEndpoint { get; set; }
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
get;
@ -80,22 +69,24 @@ namespace BTCPayServer.Configuration
public void LoadArgs(IConfiguration conf)
{
NetworkType = DefaultConfiguration.GetNetworkType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
DataDir = conf.GetDataDir(NetworkType);
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (NetworkProvider.GetNetwork(chain) == null)
if (NetworkProvider.GetNetwork<BTCPayNetworkBase>(chain) == null)
throw new ConfigException($"Invalid chains \"{chain}\"");
}
var validChains = new List<string>();
foreach (var net in NetworkProvider.GetAll())
foreach (var net in NetworkProvider.GetAll().OfType<BTCPayNetwork>())
{
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
setting.CryptoCode = net.CryptoCode;
@ -109,67 +100,28 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
$"If you have an eclair server: 'type=eclair;server=http://eclair.com:4570;password=eclairpassword;bitcoin-host=bitcoind:37393;bitcoin-auth=bitcoinrpcuser:bitcoinrpcpassword" + Environment.NewLine +
$" eclair server: 'type=eclair;server=http://eclair.com:4570;password=eclairpassword;bitcoin-host=bitcoind:37393" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
if (connectionString.IsLegacy)
else
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
void externalLnd<T>(string code, string lndType)
{
var lightning = conf.GetOrDefault<string>(code, string.Empty);
if (lightning.Length != 0)
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
}
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
if (charge.Length != 0)
{
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
chargeError ?? string.Empty);
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
ExternalServices.Load(net.CryptoCode, conf);
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
@ -183,14 +135,25 @@ namespace BTCPayServer.Configuration
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
OtherExternalServices.AddOrReplace(service.Name, uri);
}
}
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
if(!string.IsNullOrEmpty(socksEndpointString))
{
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
throw new ConfigException("Invalid value for socksendpoint");
SocksEndpoint = endpoint;
}
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -250,7 +213,6 @@ namespace BTCPayServer.Configuration
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
@ -277,12 +239,6 @@ namespace BTCPayServer.Configuration
settings.Username = "root";
}
}
else if (externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
@ -296,9 +252,9 @@ namespace BTCPayServer.Configuration
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
@ -311,30 +267,18 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
public bool BundleJsCss
{
get;
set;
}
public bool AllowAdminRegistration { get; set; }
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
public SSHSettings SSHSettings
{
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
public string TorrcFile { get; set; }
}
}

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using NBitcoin;
namespace BTCPayServer.Configuration
{
@ -57,5 +58,17 @@ namespace BTCPayServer.Configuration
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
}
}
public static string GetDataDir(this IConfiguration configuration)
{
var networkType = DefaultConfiguration.GetNetworkType(configuration);
return GetDataDir(configuration, networkType);
}
public static string GetDataDir(this IConfiguration configuration, NetworkType networkType)
{
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory);
}
}
}

@ -29,10 +29,10 @@ namespace BTCPayServer.Configuration
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--allow-admin-registration", $"For debug only, will show a checkbox when a new user register to add himself as admin. (default: false)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
@ -41,16 +41,20 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
foreach (var network in provider.GetAll().OfType<BTCPayNetwork>())
{
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from an external wallet (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndrest", $"The LND REST configuration BTCPay will expose to easily connect to the internal lnd wallet from an external wallet (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalrtl", $"The Ride the Lightning configuration so BTCPay will expose to easily open it in server settings (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue);
}
@ -119,7 +123,7 @@ namespace BTCPayServer.Configuration
builder.AppendLine("#mysql=User ID=root;Password=myPassword;Host=localhost;Port=3306;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll().OfType<BTCPayNetwork>())
{
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");

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

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

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

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

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

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

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

@ -14,8 +14,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
[EnableCors(CorsPolicies.All)]
[BitpayAPIConstraint()]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -36,20 +35,19 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
if (request == null)
throw new BitpayHttpException(400, "The request body is missing");
PairingCodeEntity pairingEntity = null;
if (string.IsNullOrEmpty(request.PairingCode))
{
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required");
if (string.IsNullOrEmpty(request.Facade))
throw new BitpayHttpException(400, "'facade' property is required");
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = pairingCode,
Facade = request.Facade,
Label = request.Label
});
@ -85,7 +83,7 @@ namespace BTCPayServer.Controllers
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = pairingEntity.Facade,
Facade = "merchant",
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}

@ -18,6 +18,9 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using BTCPayServer.Security;
using System.Globalization;
using BTCPayServer.Services.U2F;
using BTCPayServer.Services.U2F.Models;
using Newtonsoft.Json;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
@ -33,6 +36,8 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly U2FService _u2FService;
ILogger _logger;
public AccountController(
@ -42,7 +47,9 @@ namespace BTCPayServer.Controllers
SignInManager<ApplicationUser> signInManager,
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options)
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService)
{
this.storeRepository = storeRepository;
_userManager = userManager;
@ -51,6 +58,8 @@ namespace BTCPayServer.Controllers
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_logger = Logs.PayServer;
}
@ -64,6 +73,8 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null)
{
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
return RedirectToLocal();
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
@ -91,8 +102,44 @@ namespace BTCPayServer.Controllers
return View(model);
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
LoginWith2faViewModel twoFModel = null;
if (user.TwoFactorEnabled)
{
// we need to do an actual sign in attempt so that 2fa can function in next step
await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
twoFModel = new LoginWith2faViewModel
{
RememberMe = model.RememberMe
};
}
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
});
}
else
{
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
@ -101,10 +148,12 @@ namespace BTCPayServer.Controllers
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
returnUrl,
model.RememberMe
LoginWith2FaViewModel = new LoginWith2faViewModel()
{
RememberMe = model.RememberMe
}
});
}
if (result.IsLockedOut)
@ -123,6 +172,71 @@ namespace BTCPayServer.Controllers
return View(model);
}
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return new LoginWithU2FViewModel()
{
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = JsonConvert.SerializeObject(u2fChallenge),
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
{
return NotFound();
}
var errorMessage = string.Empty;
try
{
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Exception e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithU2FViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
@ -135,10 +249,13 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;
return View(model);
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
});
}
[HttpPost]
@ -175,7 +292,11 @@ namespace BTCPayServer.Controllers
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
});
}
}
@ -249,6 +370,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
return View();
}
@ -259,6 +381,7 @@ namespace BTCPayServer.Controllers
{
ViewData["ReturnUrl"] = returnUrl;
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(HomeController.Index), "Home");
@ -270,7 +393,7 @@ namespace BTCPayServer.Controllers
{
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)
if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration))
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
@ -524,9 +647,9 @@ namespace BTCPayServer.Controllers
}
}
private IActionResult RedirectToLocal(string returnUrl)
private IActionResult RedirectToLocal(string returnUrl = null)
{
if (Url.IsLocalUrl(returnUrl))
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}

@ -1,51 +1,26 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class CrowdfundAppUpdated
public class AppUpdated
{
public string AppId { get; set; }
public CrowdfundSettings Settings { get; set; }
public object Settings { get; set; }
public string StoreId { get; set; }
}
public class CrowdfundSettings
{
public string Title { get; set; }
public string Description { get; set; }
public bool Enabled { get; set; } = false;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string TargetCurrency { get; set; }
public decimal? TargetAmount { get; set; }
public bool EnforceTargetAmount { get; set; }
public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public string EmbeddedCSS { get; set; }
public string PerksTemplate { get; set; }
public bool DisqusEnabled { get; set; }= false;
public bool SoundsEnabled { get; set; }= true;
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; } = true;
public bool UseInvoiceAmount { get; set; } = true;
public int ResetEveryAmount { get; set; } = 1;
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
public bool UseAllStoreInvoices { get; set; }
public bool DisplayPerksRanking { get; set; }
public bool SortPerksByPopularity { get; set; }
public override string ToString()
{
return String.Empty;
}
}
@ -59,7 +34,9 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Title = settings.Title,
StoreId = app.StoreDataId,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
@ -77,13 +54,15 @@ namespace BTCPayServer.Controllers
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
UseInvoiceAmount = settings.UseInvoiceAmount,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
UseAllStoreInvoices = settings.UseAllStoreInvoices,
UseAllStoreInvoices = app.TagAllInvoices,
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View(vm);
}
@ -91,12 +70,12 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _AppsHelper.GetCurrencyData(vm.TargetCurrency, false) == null)
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
}
catch
{
@ -117,6 +96,24 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
}
var parsedSounds = vm.Sounds.Split(
new[] {"\r\n", "\r", "\n"},
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
}
var parsedAnimationColors = vm.AnimationColors.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
}
if (!ModelState.IsValid)
{
@ -133,15 +130,16 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate,
StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency,
Description = _AppsHelper.Sanitize( vm.Description),
EndDate = vm.EndDate,
Description = _htmlSanitizer.Sanitize( vm.Description),
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
@ -150,15 +148,17 @@ namespace BTCPayServer.Controllers
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
UseInvoiceAmount = vm.UseInvoiceAmount,
UseAllStoreInvoices = vm.UseAllStoreInvoices,
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
};
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
await UpdateAppSettings(app);
_EventAggregator.Publish(new CrowdfundAppUpdated()
_EventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -55,12 +56,16 @@ namespace BTCPayServer.Controllers
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
@ -73,6 +78,9 @@ namespace BTCPayServer.Controllers
public string CustomCSSLink { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
public bool? RedirectAutomatically { get; set; }
}
[HttpGet]
@ -83,19 +91,27 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Id = appId,
StoreId = app.StoreDataId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink
CustomCSSLink = settings.CustomCSSLink,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
};
if (HttpContext?.Request != null)
{
@ -116,7 +132,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
@ -138,11 +154,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.Template, vm.Currency);
_AppService.Parse(vm.Template, vm.Currency);
}
catch
{
@ -160,13 +176,19 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
});
await UpdateAppSettings(app);
StatusMessage = "App updated";
@ -180,6 +202,7 @@ namespace BTCPayServer.Controllers
ctx.Apps.Add(app);
ctx.Entry<AppData>(app).State = EntityState.Modified;
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
await ctx.SaveChangesAsync();
}
}

@ -7,6 +7,9 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -26,20 +29,29 @@ namespace BTCPayServer.Controllers
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
EmailSenderFactory emailSenderFactory,
AppService AppService)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_emailSenderFactory = emailSenderFactory;
_AppService = AppService;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly EmailSenderFactory _emailSenderFactory;
private AppService _AppService;
[TempData]
public string StatusMessage { get; set; }
@ -47,7 +59,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListApps()
{
var apps = await _AppsHelper.GetAllApps(GetUserId());
var apps = await _AppService.GetAllApps(GetUserId());
return View(new ListAppsViewModel()
{
Apps = apps
@ -61,7 +73,7 @@ namespace BTCPayServer.Controllers
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await _AppsHelper.DeleteApp(appData))
if (await _AppService.DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
@ -70,13 +82,13 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
@ -90,13 +102,13 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
@ -160,7 +172,7 @@ namespace BTCPayServer.Controllers
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}
@ -168,5 +180,10 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
private async Task<bool> IsEmailConfigured(string storeId)
{
return (await (_emailSenderFactory.GetEmailSender(storeId) as EmailSender)?.GetEmailSettings())?.IsComplete() is true;
}
}
}

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